needtoknow 1.0.0__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.
- needtoknow-1.0.0/.gitignore +13 -0
- needtoknow-1.0.0/CHANGELOG.md +107 -0
- needtoknow-1.0.0/LICENSE +21 -0
- needtoknow-1.0.0/PKG-INFO +297 -0
- needtoknow-1.0.0/README.md +250 -0
- needtoknow-1.0.0/needtoknow/__init__.py +41 -0
- needtoknow-1.0.0/needtoknow/authorizers/__init__.py +1 -0
- needtoknow-1.0.0/needtoknow/authorizers/opa.py +112 -0
- needtoknow-1.0.0/needtoknow/authorizers/openfga.py +164 -0
- needtoknow-1.0.0/needtoknow/authz.py +104 -0
- needtoknow-1.0.0/needtoknow/backends/__init__.py +5 -0
- needtoknow-1.0.0/needtoknow/backends/chroma.py +156 -0
- needtoknow-1.0.0/needtoknow/backends/memory.py +60 -0
- needtoknow-1.0.0/needtoknow/backends/pgvector.py +189 -0
- needtoknow-1.0.0/needtoknow/caching.py +156 -0
- needtoknow-1.0.0/needtoknow/events.py +76 -0
- needtoknow-1.0.0/needtoknow/filters.py +128 -0
- needtoknow-1.0.0/needtoknow/langchain.py +100 -0
- needtoknow-1.0.0/needtoknow/llamaindex.py +96 -0
- needtoknow-1.0.0/needtoknow/otel.py +55 -0
- needtoknow-1.0.0/needtoknow/py.typed +0 -0
- needtoknow-1.0.0/needtoknow/retriever.py +327 -0
- needtoknow-1.0.0/needtoknow/types.py +45 -0
- needtoknow-1.0.0/pyproject.toml +71 -0
- needtoknow-1.0.0/tests/helpers.py +116 -0
- needtoknow-1.0.0/tests/test_backends.py +161 -0
- needtoknow-1.0.0/tests/test_caching_events.py +189 -0
- needtoknow-1.0.0/tests/test_chroma_probe.py +44 -0
- needtoknow-1.0.0/tests/test_core.py +202 -0
- needtoknow-1.0.0/tests/test_filters.py +63 -0
- needtoknow-1.0.0/tests/test_frameworks.py +118 -0
- needtoknow-1.0.0/tests/test_opa.py +156 -0
- needtoknow-1.0.0/tests/test_openfga.py +158 -0
- needtoknow-1.0.0/tests/test_properties.py +55 -0
- needtoknow-1.0.0/tests/test_translators.py +89 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes are recorded here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/). From 1.0 on, the public API is stable.
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [1.0.0] - 2026-10-16
|
|
10
|
+
|
|
11
|
+
The first stable release. The public API in the
|
|
12
|
+
[API reference](https://muzakkirhussain011.github.io/needtoknow/api.html) now follows semantic
|
|
13
|
+
versioning. Everything in 1.0.0rc1 below is included; these are the changes since the release
|
|
14
|
+
candidate.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- Tested on Python 3.14. Every Python version CPython still supports is supported, and a version is
|
|
19
|
+
dropped in the first minor release after its end of life (Python 3.10's is 2026-10-31).
|
|
20
|
+
- A release procedure for maintainers in CONTRIBUTING.
|
|
21
|
+
|
|
22
|
+
### Removed
|
|
23
|
+
|
|
24
|
+
- `RetrievalEvent.extra`, a field nothing ever set. Audit lines no longer carry an empty `"extra"`.
|
|
25
|
+
Removed before 1.0 so that it is not frozen into the stable API; adding a field later is
|
|
26
|
+
compatible, removing one is not.
|
|
27
|
+
|
|
28
|
+
## [1.0.0rc1] - 2026-10-16
|
|
29
|
+
|
|
30
|
+
The first release candidate for 1.0. The public API in the
|
|
31
|
+
[API reference](https://muzakkirhussain011.github.io/needtoknow/api.html) is what 1.0.0 will ship:
|
|
32
|
+
changes before then are fixes, unless testing the release candidate shows that a breaking change is
|
|
33
|
+
needed, in which case it will be announced in a second release candidate. Install it with
|
|
34
|
+
`pip install --pre needtoknow`.
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- `PermissionedRetriever` with `prefilter`, `postfilter` and `auto` strategies, sync
|
|
39
|
+
(`retrieve`) and async (`aretrieve`) entry points sharing one implementation.
|
|
40
|
+
- Failing closed: any authorizer error, wrong number of decisions or non-boolean decision raises
|
|
41
|
+
`AuthorizationError` and returns nothing.
|
|
42
|
+
- Re-check of pre-filtered results, with `RetrievalResult.violations` counting hits that a
|
|
43
|
+
backend returned despite the filter.
|
|
44
|
+
- A backend-neutral filter language (`In`, `And`, `Or`) with reference semantics in `matches`;
|
|
45
|
+
empty `And`/`Or` are rejected because an empty conjunction would mean "allow everything".
|
|
46
|
+
- `MetadataACLAuthorizer` (permissions stored on each chunk) and `FunctionAuthorizer` (your own
|
|
47
|
+
logic).
|
|
48
|
+
- `InMemoryBackend` with deterministic keyword scoring, for tests and small corpora.
|
|
49
|
+
- Property-based tests (Hypothesis) for the security guarantees on Python 3.10 to 3.13.
|
|
50
|
+
- Contributor documentation: CONTRIBUTING, GOVERNANCE, CODE_OF_CONDUCT, SECURITY.
|
|
51
|
+
- `needtoknow.backends.pgvector`: PostgreSQL + pgvector backend with the permission filter in the
|
|
52
|
+
query as JSON containment tests (GIN-indexable), iterative HNSW scans, `create_schema`, and a
|
|
53
|
+
native async search when given an async pool. Extra: `needtoknow[pgvector]`.
|
|
54
|
+
- `needtoknow.backends.chroma`: Chroma backend translating filters into `where` clauses. It never
|
|
55
|
+
stores Chroma array metadata: in chromadb 1.5.x (local mode) a deleted collection's array
|
|
56
|
+
metadata surfaces on new records in other collections and is used by `where` filters
|
|
57
|
+
(chroma-core/chroma#7594). List values become
|
|
58
|
+
one boolean field each, and search results are rebuilt from an exact JSON copy of the metadata,
|
|
59
|
+
so the re-check always sees the true permissions. Extra: `needtoknow[chroma]`.
|
|
60
|
+
- `needtoknow.authorizers.opa`: Open Policy Agent authorizer using only the standard library, with
|
|
61
|
+
per-chunk decisions and optional policy-provided filters; every failure fails closed. Example
|
|
62
|
+
policy in `examples/opa`.
|
|
63
|
+
- `filter_to_dict` / `filter_from_dict` for a JSON form of filters.
|
|
64
|
+
- `needtoknow.authorizers.openfga`: OpenFGA authorizer over the HTTP API (no extra dependency):
|
|
65
|
+
batched BatchCheck, ListObjects pre-filtering that falls back to post-filtering when the list may
|
|
66
|
+
be truncated, and the subject's groups sent as contextual tuples. Tested against a real OpenFGA
|
|
67
|
+
for random relationship graphs.
|
|
68
|
+
- `needtoknow.langchain`: `NeedToKnowRetriever` (a LangChain retriever; refuses to run without a
|
|
69
|
+
subject) and `LangChainVectorStoreBackend` (any LangChain vector store, post-filtered).
|
|
70
|
+
Extra: `needtoknow[langchain]`.
|
|
71
|
+
- `needtoknow.llamaindex`: `NeedToKnowRetriever`, `PermissionNodePostprocessor` and
|
|
72
|
+
`LlamaIndexBackend`. Extra: `needtoknow[llamaindex]`.
|
|
73
|
+
- Runnable examples for LangChain and LlamaIndex, executed in CI.
|
|
74
|
+
- `CachingAuthorizer`: caches pre-filters and per-chunk decisions (keyed by chunk id and
|
|
75
|
+
metadata) per subject for `ttl` seconds, with that as the documented revocation bound; errors and
|
|
76
|
+
malformed answers are never cached; `invalidate()`; sync and async.
|
|
77
|
+
- Retrieval events: every call, including failed ones, produces a `RetrievalEvent` for
|
|
78
|
+
`observers` (subject id, strategy, returned ids, denied and violation counts, duration, error
|
|
79
|
+
type, optional HMAC of the query). Never query or chunk text. `JsonLinesAudit` writes them as
|
|
80
|
+
JSON lines; observer failures are logged and never affect retrieval.
|
|
81
|
+
- `needtoknow.otel.OpenTelemetryObserver`: one span per retrieval with counts and outcomes, the
|
|
82
|
+
subject id only on request. Extra: `needtoknow[otel]`.
|
|
83
|
+
- Documentation site (Sphinx, MyST, Furo) in `docs/`: getting started, concepts, a security model,
|
|
84
|
+
operations, one page per integration, extending needtoknow, benchmarks and an API reference.
|
|
85
|
+
CI builds it with warnings as errors, and every push to `main` publishes it to
|
|
86
|
+
<https://muzakkirhussain011.github.io/needtoknow/>. Extra: `needtoknow[docs]`.
|
|
87
|
+
- Benchmark harness (`benchmarks/`): recall@k against exact ground truth, leaks, completeness and
|
|
88
|
+
latency for exact search, Chroma and pgvector across permission selectivity and layouts, with a
|
|
89
|
+
Kaggle notebook for real embeddings. CI runs a small version against real Chroma and PostgreSQL
|
|
90
|
+
and fails on any leak.
|
|
91
|
+
- Conformance tests: every backend must return exactly the documents the reference semantics
|
|
92
|
+
select, for random metadata and nested filters, and never leak through the retriever. CI runs
|
|
93
|
+
them against real PostgreSQL, Chroma and OPA.
|
|
94
|
+
|
|
95
|
+
### Changed
|
|
96
|
+
|
|
97
|
+
- `RetrievalResult.complete` is documented precisely: it is `False` when needtoknow knows results
|
|
98
|
+
may be missing, and it trusts the backend's ranking, so it cannot see results an approximate index
|
|
99
|
+
failed to return. The benchmarks measure how often that happens.
|
|
100
|
+
- Filter values must be strings or integers, and metadata values of other types (booleans,
|
|
101
|
+
floats) never match. This keeps every backend's answer identical to the reference.
|
|
102
|
+
- The project is now `needtoknow`, a permission-aware retrieval library (ADR 0005). The earlier
|
|
103
|
+
Rampart gateway lives on as an example deployment in `examples/gateway`.
|
|
104
|
+
|
|
105
|
+
[Unreleased]: https://github.com/muzakkirhussain011/needtoknow/compare/v1.0.0...HEAD
|
|
106
|
+
[1.0.0]: https://github.com/muzakkirhussain011/needtoknow/compare/v1.0.0rc1...v1.0.0
|
|
107
|
+
[1.0.0rc1]: https://github.com/muzakkirhussain011/needtoknow/releases/tag/v1.0.0rc1
|
needtoknow-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Syed Muzakkir Hussain
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: needtoknow
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Permission-aware retrieval for RAG: every user gets only the chunks they are allowed to read.
|
|
5
|
+
Project-URL: Homepage, https://github.com/muzakkirhussain011/needtoknow
|
|
6
|
+
Project-URL: Repository, https://github.com/muzakkirhussain011/needtoknow
|
|
7
|
+
Project-URL: Issues, https://github.com/muzakkirhussain011/needtoknow/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/muzakkirhussain011/needtoknow/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Documentation, https://muzakkirhussain011.github.io/needtoknow/
|
|
10
|
+
Author: Syed Muzakkir Hussain
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: access-control,authorization,langchain,llamaindex,llm,openfga,rag,retrieval,security,vector-search
|
|
14
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
25
|
+
Classifier: Topic :: Security
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.10
|
|
28
|
+
Provides-Extra: chroma
|
|
29
|
+
Requires-Dist: chromadb>=1.5; extra == 'chroma'
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: hypothesis>=6.100; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest>=8.2; extra == 'dev'
|
|
33
|
+
Requires-Dist: ruff==0.17.1; extra == 'dev'
|
|
34
|
+
Provides-Extra: docs
|
|
35
|
+
Requires-Dist: furo>=2025.12.19; extra == 'docs'
|
|
36
|
+
Requires-Dist: myst-parser>=5.1; extra == 'docs'
|
|
37
|
+
Requires-Dist: sphinx>=9.1; extra == 'docs'
|
|
38
|
+
Provides-Extra: langchain
|
|
39
|
+
Requires-Dist: langchain-core>=1.0; extra == 'langchain'
|
|
40
|
+
Provides-Extra: llamaindex
|
|
41
|
+
Requires-Dist: llama-index-core>=0.12; extra == 'llamaindex'
|
|
42
|
+
Provides-Extra: otel
|
|
43
|
+
Requires-Dist: opentelemetry-api>=1.30; extra == 'otel'
|
|
44
|
+
Provides-Extra: pgvector
|
|
45
|
+
Requires-Dist: psycopg[binary,pool]>=3.2; extra == 'pgvector'
|
|
46
|
+
Description-Content-Type: text/markdown
|
|
47
|
+
|
|
48
|
+
# needtoknow
|
|
49
|
+
|
|
50
|
+
[](https://github.com/muzakkirhussain011/needtoknow/actions/workflows/ci.yml)
|
|
51
|
+
[](https://muzakkirhussain011.github.io/needtoknow/)
|
|
52
|
+
[](https://github.com/muzakkirhussain011/needtoknow/blob/main/LICENSE)
|
|
53
|
+

|
|
54
|
+

|
|
55
|
+
|
|
56
|
+
**Permission-aware retrieval for RAG.** Every user gets only the chunks they are allowed to read,
|
|
57
|
+
whatever vector store, permission system or framework you use.
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from needtoknow import PermissionedRetriever, MetadataACLAuthorizer, Subject
|
|
61
|
+
|
|
62
|
+
retriever = PermissionedRetriever(backend, MetadataACLAuthorizer())
|
|
63
|
+
result = retriever.retrieve("What are this year's salary bands?", Subject("bob", groups=["engineering"]), k=5)
|
|
64
|
+
# result.hits holds only chunks Bob may read. HR documents never reach the prompt.
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**[Documentation](https://muzakkirhussain011.github.io/needtoknow/)** ·
|
|
68
|
+
[Security model](https://muzakkirhussain011.github.io/needtoknow/security.html) ·
|
|
69
|
+
[Benchmarks](https://muzakkirhussain011.github.io/needtoknow/benchmarks.html) ·
|
|
70
|
+
[Changelog](https://github.com/muzakkirhussain011/needtoknow/blob/main/CHANGELOG.md)
|
|
71
|
+
|
|
72
|
+
## Why
|
|
73
|
+
|
|
74
|
+
A RAG assistant over company documents is only as safe as its retrieval step. If the vector search
|
|
75
|
+
ignores who is asking, an engineer can ask the assistant about salaries and get HR documents
|
|
76
|
+
summarised back. Every team building internal RAG has to solve this, and most solve it by hand,
|
|
77
|
+
differently each time.
|
|
78
|
+
|
|
79
|
+
`needtoknow` is that layer as a small, tested library:
|
|
80
|
+
|
|
81
|
+
- **Works with what you have.** Wrap any search backend and any permission system. The core has
|
|
82
|
+
no dependencies.
|
|
83
|
+
- **Filters in the right place.** When the permission system can describe "everything Bob may
|
|
84
|
+
read" as a filter, the filter goes *inside* the vector search, so results are the true top-k of
|
|
85
|
+
what Bob may read. When it cannot, candidates are checked after the search and the search widens
|
|
86
|
+
until enough permitted results are found.
|
|
87
|
+
- **Fails closed.** If the permission service errors, times out or returns malformed answers, the
|
|
88
|
+
user gets an error, never unfiltered results.
|
|
89
|
+
- **Defence in depth.** Pre-filtered results are checked again, so a backend bug that ignores the
|
|
90
|
+
filter cannot leak. Each result reports how many such violations it caught.
|
|
91
|
+
|
|
92
|
+
## Install
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
pip install needtoknow
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Python 3.10 or newer: every Python version CPython still supports, each dropped in the first minor
|
|
99
|
+
release after its end of life. Adapters for external systems install as optional extras.
|
|
100
|
+
|
|
101
|
+
## Quickstart
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from needtoknow import Document, InMemoryBackend, MetadataACLAuthorizer, PermissionedRetriever, Subject
|
|
105
|
+
|
|
106
|
+
backend = InMemoryBackend(
|
|
107
|
+
[
|
|
108
|
+
Document("handbook", "Annual leave is 25 days.", {"allowed_groups": ["*"]}),
|
|
109
|
+
Document("salaries", "Salary bands for 2026 ...", {"allowed_groups": ["hr"]}),
|
|
110
|
+
Document("runbook", "Roll back a failed deploy ...", {"allowed_groups": ["engineering"]}),
|
|
111
|
+
]
|
|
112
|
+
)
|
|
113
|
+
retriever = PermissionedRetriever(backend, MetadataACLAuthorizer())
|
|
114
|
+
|
|
115
|
+
result = retriever.retrieve("leave salary bands", Subject("bob", groups=["engineering"]), k=3)
|
|
116
|
+
print([hit.id for hit in result.hits]) # ['handbook']
|
|
117
|
+
print(result.strategy, result.complete) # prefilter True
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`aretrieve` is the async equivalent, with the same arguments and guarantees.
|
|
121
|
+
|
|
122
|
+
## How it works
|
|
123
|
+
|
|
124
|
+
| Strategy | When | Cost | Result |
|
|
125
|
+
|---|---|---|---|
|
|
126
|
+
| `prefilter` | The authorizer can return a filter and the backend supports filters | One search | The exact top-k of what the subject may read |
|
|
127
|
+
| `postfilter` | Any authorizer | Searches `k × overfetch` candidates, widening up to `max_candidates` | Exact when enough permitted candidates exist; `complete=False` otherwise |
|
|
128
|
+
| `auto` (default) | Picks `prefilter` when both sides support it | | |
|
|
129
|
+
|
|
130
|
+
Every call returns a `RetrievalResult` with the hits plus `strategy`, `examined`, `denied`,
|
|
131
|
+
`violations` (pre-filtered hits the re-check rejected, which should always be 0) and `complete`.
|
|
132
|
+
|
|
133
|
+
## Integrations
|
|
134
|
+
|
|
135
|
+
Each integration is an optional extra, so the core stays dependency-free.
|
|
136
|
+
|
|
137
|
+
**PostgreSQL + pgvector** (`pip install "needtoknow[pgvector]"`, pgvector 0.8+). The permission
|
|
138
|
+
filter runs inside the SQL query as JSON containment tests, served by a GIN index, and iterative
|
|
139
|
+
index scans keep HNSW returning k rows under selective filters.
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from psycopg_pool import ConnectionPool
|
|
143
|
+
|
|
144
|
+
from needtoknow.backends.pgvector import PgVectorBackend, create_schema
|
|
145
|
+
|
|
146
|
+
pool = ConnectionPool("postgresql://localhost/app")
|
|
147
|
+
create_schema(pool, dim=768)
|
|
148
|
+
backend = PgVectorBackend(pool, embed=my_embedding_model) # embed: str -> list[float]
|
|
149
|
+
backend.add(documents)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Chroma** (`pip install "needtoknow[chroma]"`). The filter becomes Chroma's `where` clause. Each
|
|
153
|
+
value in a permission list is stored as its own field rather than as a Chroma array, and an exact
|
|
154
|
+
copy of the metadata is kept for the re-check: in chromadb 1.5.x, deleting a collection leaves its
|
|
155
|
+
array metadata behind for new records in other collections to pick up
|
|
156
|
+
([chroma-core/chroma#7594](https://github.com/chroma-core/chroma/issues/7594)), which for
|
|
157
|
+
permissions would be a leak.
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
import chromadb
|
|
161
|
+
|
|
162
|
+
from needtoknow.backends.chroma import ChromaBackend
|
|
163
|
+
|
|
164
|
+
collection = chromadb.PersistentClient(path="./chroma").get_or_create_collection("docs")
|
|
165
|
+
backend = ChromaBackend(collection) # uses the collection's embedding function
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**Open Policy Agent** (no extra dependency). Your Rego policy returns one decision per chunk, and
|
|
169
|
+
can optionally return a filter so the vector store does the filtering. A complete policy is in
|
|
170
|
+
[`examples/opa/needtoknow.rego`](https://github.com/muzakkirhussain011/needtoknow/blob/main/examples/opa/needtoknow.rego).
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
from needtoknow.authorizers.opa import OPAAuthorizer
|
|
174
|
+
|
|
175
|
+
authorizer = OPAAuthorizer("http://localhost:8181", filter_path="needtoknow/filter")
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**OpenFGA** (no extra dependency). Relationship-based access control in the Google Zanzibar style.
|
|
179
|
+
Checks run in batches; for pre-filtering, ListObjects returns every document the user may view,
|
|
180
|
+
and if that list may have been truncated the retriever post-filters instead. The subject's groups
|
|
181
|
+
can be sent as contextual tuples, so memberships from your identity provider need no syncing.
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
from needtoknow.authorizers.openfga import OpenFGAAuthorizer
|
|
185
|
+
|
|
186
|
+
authorizer = OpenFGAAuthorizer("http://localhost:8080", store_id, resource_field="doc_id")
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**LangChain** (`pip install "needtoknow[langchain]"`). `NeedToKnowRetriever` is a LangChain
|
|
190
|
+
retriever, so it works in chains, agents and LangGraph tools; `LangChainVectorStoreBackend` turns
|
|
191
|
+
any LangChain vector store into a needtoknow backend (post-filtered).
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
from needtoknow.langchain import NeedToKnowRetriever
|
|
195
|
+
|
|
196
|
+
retriever = NeedToKnowRetriever(retriever=PermissionedRetriever(backend, authorizer))
|
|
197
|
+
docs = retriever.invoke("What are this year's salary bands?", subject=Subject("bob", ["sales"]))
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**LlamaIndex** (`pip install "needtoknow[llamaindex]"`). `NeedToKnowRetriever` for query and chat
|
|
201
|
+
engines, `PermissionNodePostprocessor` for existing pipelines, and `LlamaIndexBackend` to wrap an
|
|
202
|
+
existing index. Runnable examples for both frameworks are in [`examples/`](https://github.com/muzakkirhussain011/needtoknow/tree/main/examples).
|
|
203
|
+
|
|
204
|
+
Filter values are strings or integers, the types permission identifiers use. That keeps every
|
|
205
|
+
backend's answer identical to the reference semantics, which the test suite checks for each one.
|
|
206
|
+
|
|
207
|
+
## Caching, audit and tracing
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
from needtoknow import CachingAuthorizer, JsonLinesAudit
|
|
211
|
+
from needtoknow.otel import OpenTelemetryObserver # pip install "needtoknow[otel]"
|
|
212
|
+
|
|
213
|
+
retriever = PermissionedRetriever(
|
|
214
|
+
backend,
|
|
215
|
+
CachingAuthorizer(authorizer, ttl=30), # a revoked permission stops granting access within 30 s
|
|
216
|
+
observers=[JsonLinesAudit(audit_file), OpenTelemetryObserver()], # audit_file: an open text file
|
|
217
|
+
audit_key=b"...", # optional: an HMAC of each query, so repeats can be matched without storing them
|
|
218
|
+
)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
`CachingAuthorizer` caches pre-filters and per-chunk decisions per subject for `ttl` seconds and
|
|
222
|
+
never caches an error; `invalidate()` drops decisions after a change you know about. Every
|
|
223
|
+
retrieval, including failed ones, produces one `RetrievalEvent` (who asked, strategy, returned ids,
|
|
224
|
+
denied and violation counts, duration, error type). Events never contain query or chunk text, and
|
|
225
|
+
an observer that fails is logged without affecting retrieval.
|
|
226
|
+
|
|
227
|
+
## Bring your own backend or permission system
|
|
228
|
+
|
|
229
|
+
A backend is any object with a `search` method:
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
class MyBackend:
|
|
233
|
+
supports_filters = True # set False if search() ignores `filter`
|
|
234
|
+
|
|
235
|
+
def search(self, query: str, k: int, filter=None) -> list[Hit]:
|
|
236
|
+
"""Return hits best first. Translate `filter` (In / And / Or) into your store's filter."""
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
An authorizer answers two questions:
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
class MyAuthorizer:
|
|
243
|
+
def check(self, subject, hits, action="read") -> list[bool]:
|
|
244
|
+
"""One decision per hit, in order. Raise on any doubt: the retriever fails closed."""
|
|
245
|
+
|
|
246
|
+
def filter_for(self, subject, action="read"):
|
|
247
|
+
"""A Filter selecting everything the subject may read, or None if you cannot express one."""
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Either can add `async` versions (`asearch`, `acheck`, `afilter_for`); otherwise the sync methods
|
|
251
|
+
run in a thread under `aretrieve`. `FunctionAuthorizer` wraps a plain function.
|
|
252
|
+
|
|
253
|
+
## Testing approach
|
|
254
|
+
|
|
255
|
+
The guarantees are tested as properties with [Hypothesis](https://hypothesis.readthedocs.io):
|
|
256
|
+
for randomly generated corpora, permissions, users and queries, every strategy must
|
|
257
|
+
|
|
258
|
+
1. never return a chunk the user may not read,
|
|
259
|
+
2. return exactly the top-k of what the user may read (checked against an independent oracle),
|
|
260
|
+
3. stay safe when the backend ignores the filter, and
|
|
261
|
+
4. give identical results through `retrieve` and `aretrieve`.
|
|
262
|
+
|
|
263
|
+
## Benchmarks
|
|
264
|
+
|
|
265
|
+
On 57,600 real chunks (BEIR FiQA, Qwen3-Embedding-0.6B), across 33,000 retrievals, no needtoknow
|
|
266
|
+
strategy returned a single forbidden chunk. Filtering inside the search kept recall@10 at 0.949 or
|
|
267
|
+
better on pgvector for users who may read anywhere from 0.06% to 100% of the corpus, including when
|
|
268
|
+
what they may read is far from what they ask about. Filtering after a top-10 search found almost
|
|
269
|
+
nothing for selective permissions. Method, tables and caveats: [Benchmarks](https://muzakkirhussain011.github.io/needtoknow/benchmarks.html).
|
|
270
|
+
|
|
271
|
+
| Readable (far from the query) | Naive top-k, then filter | needtoknow postfilter | needtoknow prefilter (pgvector) |
|
|
272
|
+
|---|---|---|---|
|
|
273
|
+
| 0.9% | 0.000 | 0.048 | 0.995 |
|
|
274
|
+
| 4.8% | 0.000 | 0.314 | 0.950 |
|
|
275
|
+
| 20% | 0.041 | 0.974 | 0.949 |
|
|
276
|
+
|
|
277
|
+
## Stability and roadmap
|
|
278
|
+
|
|
279
|
+
1.0 is the stable base: the core, the pgvector and Chroma backends, the OPA and OpenFGA authorizers,
|
|
280
|
+
the LangChain and LlamaIndex integrations, caching, audit events and tracing. The public API in the
|
|
281
|
+
[API reference](https://muzakkirhussain011.github.io/needtoknow/api.html) follows semantic
|
|
282
|
+
versioning: no breaking change without a major version and a deprecation period first.
|
|
283
|
+
|
|
284
|
+
Next, in rough order: permission sync from Google Drive, SharePoint and Confluence; Qdrant and
|
|
285
|
+
Elasticsearch backends; SpiceDB and Cerbos authorizers. Contributions are welcome; see
|
|
286
|
+
[CONTRIBUTING.md](https://github.com/muzakkirhussain011/needtoknow/blob/main/CONTRIBUTING.md).
|
|
287
|
+
|
|
288
|
+
## Example deployment
|
|
289
|
+
|
|
290
|
+
[`examples/gateway`](https://github.com/muzakkirhussain011/needtoknow/tree/main/examples/gateway) is a complete, self-hosted, OpenAI-compatible gateway built
|
|
291
|
+
on the same ideas: OPA policies, pgvector hybrid search with the ACL inside the query, PII
|
|
292
|
+
pseudonymisation, prompt-injection quarantine and a hash-chained audit log. It is being ported onto
|
|
293
|
+
this library.
|
|
294
|
+
|
|
295
|
+
## License
|
|
296
|
+
|
|
297
|
+
[MIT](https://github.com/muzakkirhussain011/needtoknow/blob/main/LICENSE)
|