Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
4fd03db
init rbac
shivamka1 Mar 4, 2026
16d21c0
impl introspection
shivamka1 Mar 4, 2026
fc54da6
add introspection for schema as well, add test
shivamka1 Mar 4, 2026
4e91a80
impl raphtory-auth member mod, impl permissions as gql apis, fix tests
shivamka1 Mar 10, 2026
c0251a7
fix circular dep
shivamka1 Mar 10, 2026
11eee0c
fmt
shivamka1 Mar 10, 2026
506234a
fix build
shivamka1 Mar 10, 2026
f463a79
fix tests
shivamka1 Mar 11, 2026
9499302
impl permissions read gql, add tests
shivamka1 Mar 11, 2026
af9d943
filters
shivamka1 Mar 13, 2026
676b639
fix test, fmt
shivamka1 Mar 13, 2026
2a11017
fix test
shivamka1 Mar 16, 2026
599b14d
fix workflow
shivamka1 Mar 16, 2026
fae63b2
fix tests
shivamka1 Mar 16, 2026
41948a2
Merge branch 'db_v4' into features/rbac
shivamka1 Mar 16, 2026
b20242c
chore: apply tidy-public auto-fixes
github-actions[bot] Mar 16, 2026
b332f7c
impl heirarchy based permissions, fix tests
shivamka1 Mar 17, 2026
6f977d8
Merge branch 'features/rbac' of github.com:Pometry/Raphtory into feat…
shivamka1 Mar 17, 2026
ddce715
ren "a" to "access"
shivamka1 Mar 17, 2026
a74acce
chore: apply tidy-public auto-fixes
github-actions[bot] Mar 17, 2026
1e7ef4e
ref
shivamka1 Mar 17, 2026
3952759
ref
shivamka1 Mar 17, 2026
9494b26
ref
shivamka1 Mar 17, 2026
3ca3f99
fmt
shivamka1 Mar 17, 2026
08230a6
Merge branch 'features/rbac' of github.com:Pometry/Raphtory into feat…
shivamka1 Mar 17, 2026
659b7d2
add client tests
shivamka1 Mar 17, 2026
bc800f5
skip none
shivamka1 Mar 17, 2026
00d831b
introspect should allow access to metagraph only to avoid loading gra…
shivamka1 Mar 17, 2026
f85f5a9
chore: apply tidy-public auto-fixes
github-actions[bot] Mar 17, 2026
4b43a78
impl graph metadata gql api, add tests
shivamka1 Mar 19, 2026
210d40b
gate vectorised graph and receive graph behind read gate
shivamka1 Mar 19, 2026
2471998
impl namespace_permissions, add/fix tests, update postman collection
shivamka1 Mar 26, 2026
cca37e1
ref
shivamka1 Mar 26, 2026
115510a
ref
shivamka1 Mar 26, 2026
8576735
fix permissions
shivamka1 Mar 28, 2026
24d52d8
ref
shivamka1 Mar 28, 2026
58a1da0
best match namespace permission resolution
shivamka1 Mar 28, 2026
3522dca
ref
shivamka1 Mar 28, 2026
1f0f43b
fix at least read/write
shivamka1 Mar 28, 2026
49302ba
intro levels and ordering of permissions
shivamka1 Mar 28, 2026
b6f3e0a
remove fail-open in require_jwt_write_access
shivamka1 Mar 28, 2026
ed5148b
req ns write perm
shivamka1 Mar 28, 2026
b9b9dab
gate permissions api
shivamka1 Mar 28, 2026
579839d
ref
shivamka1 Mar 28, 2026
e463dae
rid dead results
shivamka1 Mar 28, 2026
e58ac6f
add discover tests
shivamka1 Mar 28, 2026
81912f5
change fail open to fail close, add test
shivamka1 Mar 30, 2026
c427eac
rid wild card, add test
shivamka1 Mar 30, 2026
9b86914
fix inference issues
shivamka1 Mar 30, 2026
ada4320
fix inference, add test
shivamka1 Mar 30, 2026
47c04ab
impl filtered receiveGraph
shivamka1 Mar 30, 2026
cdad1dd
fix clam-core version
shivamka1 Mar 30, 2026
54c5b68
make rbac explicit by passing permissions store, fix tests
shivamka1 Mar 30, 2026
8baa176
GraphAccessFilter is now a OneOfInput enum
shivamka1 Mar 30, 2026
09c8d69
ref
shivamka1 Mar 30, 2026
84679e3
remove default PermissionsPlugin from schema; add conditional RBAC re…
shivamka1 Mar 31, 2026
0b970e2
raphtory-graphql/src/main.rs deleted. raphtory-server is now the sing…
shivamka1 Mar 31, 2026
67d9935
fix: permission denial in graph/graphMetadata returns null not error,…
shivamka1 Mar 31, 2026
3702d65
fmt
shivamka1 Mar 31, 2026
387ecda
fix error messages
shivamka1 Mar 31, 2026
95d20d8
intro AuthPolicyError
shivamka1 Mar 31, 2026
3dd54d2
fix tests
shivamka1 Mar 31, 2026
27af2e5
Merge branch 'db_v4' into features/rbac
shivamka1 Mar 31, 2026
8e082aa
support RSA
shivamka1 Apr 1, 2026
f6021e0
Merge branch 'features/rbac' of github.com:Pometry/Raphtory into feat…
shivamka1 Apr 1, 2026
1b06715
use raphtory-server binary in stress test workflow
shivamka1 Apr 1, 2026
cd8dfb3
fix test
shivamka1 Apr 1, 2026
6a70667
fix tests
shivamka1 Apr 2, 2026
6e6e145
chore: apply tidy-public auto-fixes
github-actions[bot] Apr 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/bench-graphql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@ jobs:
k6-version: '1.0.0'
- name: Run GraphQL benchmarks
run: cd graphql-bench && make bench-local
- name: Restore metadata file
run: git restore graphql-bench/data/apache/master # otherwise github-action-benchmark fails to create the commit
- name: Restore modified files
run: |
git restore Cargo.lock # modified by build; github-action-benchmark can't switch to gh-pages with dirty working tree
git restore graphql-bench/data/apache/master # otherwise github-action-benchmark fails to create the commit
- name: Print bench results
run: cat graphql-bench/output.json
- name: Store benchmark results from master branch
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ jobs:
run: |
set -o pipefail
cargo bench --bench base --bench algobench -p raphtory-benchmark -- --output-format=bencher | tee benchmark-result.txt
- name: Delete cargo.lock if it exists
run: rm -f Cargo.lock
- name: Restore Cargo.lock to avoid dirty working tree
run: git checkout -- Cargo.lock
- name: Store benchmark results from master branch
if: github.ref == 'refs/heads/master'
uses: benchmark-action/github-action-benchmark@v1
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/stress-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ jobs:
env:
RUST_BACKTRACE: 1
run: |
cargo build --package raphtory-graphql --bin raphtory-graphql --profile=build-fast
./target/build-fast/raphtory-graphql server --work-dir graphs &
cargo build --package raphtory-server --bin raphtory-server --profile=build-fast
./target/build-fast/raphtory-server server --work-dir graphs &
cd graphql-bench
make stress-test
- name: Upload k6 report
Expand Down
14 changes: 14 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 9 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@ members = [
"examples/custom-gql-apis",
"python",
"raphtory-graphql",
"raphtory-auth-noop",
"raphtory-server",
"raphtory-api",
"raphtory-core",
"raphtory-storage",
"raphtory-api-macros",
"raphtory-itertools",
"clam-core",
"clam-core/snb"
, "raphtory-itertools"]
"clam-core/snb",
"raphtory-itertools"
]
default-members = ["raphtory"]
exclude = ["optd"]
resolver = "2"
Expand Down Expand Up @@ -193,3 +196,7 @@ disjoint-sets = "0.4.2"
[workspace.dependencies.storage]
package = "db4-storage"
path = "db4-storage"

[workspace.dependencies.auth]
package = "raphtory-auth-noop"
path = "raphtory-auth-noop"
20 changes: 18 additions & 2 deletions docs/reference/graphql/graphql_API.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,26 @@ Hello world demo
</tr>
<tr>
<td colspan="2" valign="top"><strong id="queryroot.graph">graph</strong></td>
<td valign="top"><a href="#graph">Graph</a>!</td>
<td valign="top"><a href="#graph">Graph</a></td>
<td>

Returns a graph

</td>
</tr>
<tr>
<td colspan="2" align="right" valign="top">path</td>
<td valign="top"><a href="#string">String</a>!</td>
<td></td>
</tr>
<tr>
<td colspan="2" valign="top"><strong id="queryroot.graphmetadata">graphMetadata</strong></td>
<td valign="top"><a href="#metagraph">MetaGraph</a></td>
<td>

Returns lightweight metadata for a graph (node/edge counts, timestamps) without loading it.
Requires at least INTROSPECT permission.

</td>
</tr>
<tr>
Expand Down Expand Up @@ -126,7 +141,8 @@ Returns a plugin.
<td valign="top"><a href="#string">String</a>!</td>
<td>

Encodes graph and returns as string
Encodes graph and returns as string.
If the caller has filtered access, the returned graph is a materialized view of the filter.

Returns:: Base64 url safe encoded string

Expand Down
4 changes: 2 additions & 2 deletions python/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ raphtory = { workspace = true, features = [
raphtory-graphql = { workspace = true, features = [
"python",
] }
clam-core = { path = "../clam-core", version = "0.17.0", features = ["python"] }

auth = { workspace = true }
clam-core = { workspace = true, features = ["python"] }

[features]
extension-module = ["pyo3/extension-module"]
Expand Down
8 changes: 6 additions & 2 deletions python/python/raphtory/graphql/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ __all__ = [
"decode_graph",
"schema",
"cli",
"has_permissions_extension",
]

class GraphServer(object):
Expand All @@ -61,7 +62,7 @@ class GraphServer(object):
otlp_tracing_service_name (str, optional): The OTLP tracing service name
config_path (str | PathLike, optional): Path to the config file
auth_public_key:
auth_enabled_for_reads:
require_auth_for_reads:
create_index:
"""

Expand All @@ -77,9 +78,10 @@ class GraphServer(object):
otlp_agent_port: Optional[str] = None,
otlp_tracing_service_name: Optional[str] = None,
auth_public_key: Any = None,
auth_enabled_for_reads: Any = None,
require_auth_for_reads: Any = None,
config_path: Optional[str | PathLike] = None,
create_index: Any = None,
permissions_store_path=None,
) -> GraphServer:
"""Create and return a new object. See help(type) for accurate signature."""

Expand Down Expand Up @@ -780,3 +782,5 @@ def schema():
"""

def cli(): ...
def has_permissions_extension():
"""Returns True if the permissions extension (raphtory-auth) is compiled in."""
3 changes: 2 additions & 1 deletion python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ use raphtory_graphql::python::pymodule::base_graphql_module;
/// Raphtory graph analytics library
#[pymodule]
fn _raphtory(py: Python<'_>, m: &Bound<PyModule>) -> PyResult<()> {
let _ = add_raphtory_classes(m);
auth::init();
add_raphtory_classes(m)?;

let graphql_module = base_graphql_module(py)?;
let algorithm_module = base_algorithm_module(py)?;
Expand Down
106 changes: 101 additions & 5 deletions python/tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,48 @@

RAPHTORY = "http://localhost:1736"

READ_JWT = jwt.encode({"a": "ro"}, PRIVATE_KEY, algorithm="EdDSA")
READ_JWT = jwt.encode({"access": "ro"}, PRIVATE_KEY, algorithm="EdDSA")
READ_HEADERS = {
"Authorization": f"Bearer {READ_JWT}",
}

WRITE_JWT = jwt.encode({"a": "rw"}, PRIVATE_KEY, algorithm="EdDSA")
WRITE_JWT = jwt.encode({"access": "rw"}, PRIVATE_KEY, algorithm="EdDSA")
WRITE_HEADERS = {
"Authorization": f"Bearer {WRITE_JWT}",
}

# openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out rsa-key.pem
# openssl pkey -in rsa-key.pem -pubout -outform DER | base64 | tr -d '\n'
RSA_PUB_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4sqe3DlHB/DaSm8Ab99yKj0KDc/WZGFPwXeTbPwCMKKSEc8zuSuIZc/fHXLSORn1apMnDq3aLryfPwyNTbpvhGiYVyp76XQGwSlN+EF2TsJZVAzp4/EI+bnHeHyv2Yc5q6AkFtoBPNtAz2P/18g7Yv/eZqNNSd7FOeuRFRs9y0LkswvMelQmoMOK7UKdC00AyiGksvFvljNC70VT9b0uVHggJwUYT0hdCbdaDj2fCJZBEmTqBBr97u3fIHo5T41sIEEPgE2j368mI+uk6V1saEU1BU+hkcq56TabgVqUYZTln5Rdm1MuBsNz+NQwOmVxgPNo45H2cNwTfsPDAAESlwIDAQAB"
RSA_PRIVATE_KEY = """-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDiyp7cOUcH8NpK
bwBv33IqPQoNz9ZkYU/Bd5Ns/AIwopIRzzO5K4hlz98dctI5GfVqkycOrdouvJ8/
DI1Num+EaJhXKnvpdAbBKU34QXZOwllUDOnj8Qj5ucd4fK/ZhzmroCQW2gE820DP
Y//XyDti/95mo01J3sU565EVGz3LQuSzC8x6VCagw4rtQp0LTQDKIaSy8W+WM0Lv
RVP1vS5UeCAnBRhPSF0Jt1oOPZ8IlkESZOoEGv3u7d8gejlPjWwgQQ+ATaPfryYj
66TpXWxoRTUFT6GRyrnpNpuBWpRhlOWflF2bUy4Gw3P41DA6ZXGA82jjkfZw3BN+
w8MAARKXAgMBAAECggEAWIH78nU2B97Syja8xGw/KUXODSreACnMDvRkKCXHkwR3
HhUvmeXn4tf3uo3rhhZf5TpNhViK7C93tIrpAHswd0u8nFP7rNW3px3ADJE7oywM
4ZTymJ8iQhdjRd3fYPT5qEWkn/hvgDkO94EOwT8nEhFKUeMMUDZs4RhSdBrACHk0
CrOC2S9xbgYb5OWGV6vkSqNB0k0Kv+LxU8sS46BLE7DxfpzSXDyeYaCAkk+wbwfb
hX7lysczbSl5l5Bulcf/LHL4Oa/5t+NcBZqyN6ylRXyqQ8LEdK4+TOJfvnePX1go
3rG4rtyaBCuW5JD1ytxUsyfh8WE4GinUbHWzxvaYQQKBgQD5PxF2CmqMY6yiaxU3
0LFtRS9DtwIPnPX3Wdchq7ivSU1W6sHJjNfyEggi10DSOOINalRM/ZnVlDo8hJ3A
SybESWWzLuDZNAAAWkmoir0UpnURz847tKd8hJUivhsbdQBeKwaCuepcW6Hdwzh1
JsJjXPovrzVGQe5FSRfBy7gswQKBgQDo78p/jEVHzuxHqSn3AsOdBdMZvPavpHb2
Bx7tRhZOOp2QiGUHZLfjI++sQQyTu1PJqmmxOOF+eD/zkqCkLLeZsmRYOQVDOQDM
Z+u+zKYRj7KaWBeGB2Oy/WEU0pGnhyMB/T5iHmroO0Hn4gDHqkEDvwFI7SUjLNAK
1RjTxVgdVwKBgCRHNMBspbOHcoI1eeIk4x5Xepitk4Q4QWjeT7zb5MbGsZYcF1bB
xFC8pSiFEi9HDkgLmPeX1gNLTuquFtP9XEgnssDQ6vNSaUmj2qLIhtrxm4qbJ5Zz
JgmutpJW/1UQw5vxQUJX0y/cOoQvvRD4MkUKLHQyWVu/jvHQwL95anZBAoGBAIrZ
9aGWYe3uINaOth8yHJzLTgz3oS0OIoOBtyPFNaKoOihfxalklmDlmQbbN74QWl/K
H3qu52vWDnkJHI0Awujxd/NG+iYaIqm2AMcZgpzRRavPeyY/3WRiua4J3x035txW
swsWCrAoMp8hD0n16Q9smj14bzzKh7ENWeFSr7W9AoGBAMOSyRdVQxVHXagh3fAa
+FNbR8pFmQC6bQGCO74DzGe6uKYpgu+XD1yinufwwsXxjieDXCHkKTGR92Kzp5VY
Hp6HhhhCcXICRRnbxhvdpyaDbCQrT522bqRJ4rNmSVYOQQiD2vng/HVB2oWMVwa+
fEtYNjbxjhX9qInHjHxeaNOp
-----END PRIVATE KEY-----"""

NEW_TEST_GRAPH = """mutation { newGraph(path:"test", graphType:EVENT) }"""

QUERY_NAMESPACES = """query { namespaces { list{ path} } }"""
Expand All @@ -54,7 +86,7 @@ def test_expired_token():
work_dir = tempfile.mkdtemp()
with GraphServer(work_dir, auth_public_key=PUB_KEY).start():
exp = time() - 100
token = jwt.encode({"a": "ro", "exp": exp}, PRIVATE_KEY, algorithm="EdDSA")
token = jwt.encode({"access": "ro", "exp": exp}, PRIVATE_KEY, algorithm="EdDSA")
headers = {
"Authorization": f"Bearer {token}",
}
Expand All @@ -63,7 +95,7 @@ def test_expired_token():
)
assert response.status_code == 401

token = jwt.encode({"a": "rw", "exp": exp}, PRIVATE_KEY, algorithm="EdDSA")
token = jwt.encode({"access": "rw", "exp": exp}, PRIVATE_KEY, algorithm="EdDSA")
headers = {
"Authorization": f"Bearer {token}",
}
Expand Down Expand Up @@ -94,7 +126,7 @@ def test_default_read_access(query):
def test_disabled_read_access(query):
work_dir = tempfile.mkdtemp()
with GraphServer(
work_dir, auth_public_key=PUB_KEY, auth_enabled_for_reads=False
work_dir, auth_public_key=PUB_KEY, require_auth_for_reads=False
).start():
add_test_graph()
data = json.dumps({"query": query})
Expand Down Expand Up @@ -206,6 +238,70 @@ def test_raphtory_client():
assert g.node("test") is not None


def test_raphtory_client_write_denied_for_read_jwt():
"""RaphtoryClient initialized with a read JWT is denied write operations."""
work_dir = tempfile.mkdtemp()
with GraphServer(work_dir, auth_public_key=PUB_KEY).start():
client = RaphtoryClient(url=RAPHTORY, token=READ_JWT)
with pytest.raises(Exception, match="requires write access"):
client.new_graph("test", "EVENT")


# --- RSA JWT support ---


def test_rsa_signed_jwt_rs256_accepted():
"""Server configured with an RSA public key accepts RS256-signed JWTs."""
work_dir = tempfile.mkdtemp()
with GraphServer(work_dir, auth_public_key=RSA_PUB_KEY).start():
token = jwt.encode({"access": "ro"}, RSA_PRIVATE_KEY, algorithm="RS256")
response = requests.post(
RAPHTORY,
headers={"Authorization": f"Bearer {token}"},
data=json.dumps({"query": QUERY_ROOT}),
)
assert_successful_response(response)


def test_rsa_signed_jwt_rs512_accepted():
"""RS512 JWT is also accepted for the same RSA key (different hash, same key material)."""
work_dir = tempfile.mkdtemp()
with GraphServer(work_dir, auth_public_key=RSA_PUB_KEY).start():
token = jwt.encode({"access": "ro"}, RSA_PRIVATE_KEY, algorithm="RS512")
response = requests.post(
RAPHTORY,
headers={"Authorization": f"Bearer {token}"},
data=json.dumps({"query": QUERY_ROOT}),
)
assert_successful_response(response)


def test_eddsa_jwt_rejected_against_rsa_key():
"""EdDSA JWT is rejected when the server is configured with an RSA public key."""
work_dir = tempfile.mkdtemp()
with GraphServer(work_dir, auth_public_key=RSA_PUB_KEY).start():
token = jwt.encode({"access": "ro"}, PRIVATE_KEY, algorithm="EdDSA")
response = requests.post(
RAPHTORY,
headers={"Authorization": f"Bearer {token}"},
data=json.dumps({"query": QUERY_ROOT}),
)
assert response.status_code == 401


def test_raphtory_client_read_jwt_can_receive_graph():
"""RaphtoryClient initialized with a read JWT can download graphs."""
work_dir = tempfile.mkdtemp()
with GraphServer(work_dir, auth_public_key=PUB_KEY).start():
client = RaphtoryClient(url=RAPHTORY, token=WRITE_JWT)
client.new_graph("test", "EVENT")
client.remote_graph("test").add_node(0, "mynode")

client2 = RaphtoryClient(url=RAPHTORY, token=READ_JWT)
g = client2.receive_graph("test")
assert g.node("mynode") is not None


def test_upload_graph():
work_dir = tempfile.mkdtemp()
with GraphServer(work_dir, auth_public_key=PUB_KEY).start():
Expand Down
Loading
Loading