diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d907c7f..bf4a3e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,8 +9,9 @@ on: jobs: check: strategy: + fail-fast: false matrix: - rust: [1.85, stable, nightly] + rust: [1.86, stable, nightly] os: [ubuntu-latest, windows-latest, macOS-latest] runs-on: ${{ matrix.os }} steps: @@ -18,36 +19,46 @@ jobs: - uses: actions-rust-lang/setup-rust-toolchain@v1 with: toolchain: ${{ matrix.rust }} + components: rustfmt, clippy - name: Format check run: cargo fmt --all -- --check + continue-on-error: ${{ matrix.rust == 'nightly' }} - - name: Clippy - run: cargo clippy --all-targets --all-features -- -D warnings + - name: Clippy (1.86 compatible crates) + if: matrix.rust == '1.86' + run: cargo clippy -p aegis-core -p aegis-cli -p aegis-test-utils -p aegis-ffi --locked + + - name: Clippy (full workspace) + if: matrix.rust != '1.86' + run: cargo clippy --workspace --all-features --locked -- -D warnings + continue-on-error: ${{ matrix.rust == 'nightly' }} - name: Build - run: cargo build --workspace + run: cargo build --workspace --locked - name: Build with features - run: cargo build --workspace --features postgres,mysql + if: matrix.rust != '1.86' + run: cargo build --workspace --features postgres,mysql --locked - name: Test (default features) - run: cargo test --workspace + run: cargo test --workspace --locked - name: Test (no default features) - run: cargo test --workspace --no-default-features + run: cargo test --workspace --no-default-features --locked - name: Test (sqlite) - run: cargo test --workspace --features sqlite + run: cargo test --workspace --features sqlite --locked - name: Test (postgres) - run: cargo test --workspace --features postgres + run: cargo test --workspace --features postgres --locked - name: Test (mysql) - run: cargo test --workspace --features mysql + if: matrix.rust != '1.86' + run: cargo test --workspace --features mysql --locked - name: Test (rocksdb) - run: cargo test --workspace --features rocksdb + run: cargo test --workspace --features rocksdb --locked - name: Install wasm-pack uses: taiki-e/install-action@v2 @@ -55,26 +66,33 @@ jobs: tool: wasm-pack - name: WASM build + if: matrix.rust != '1.86' run: | cd packages/aegis-browser/rust wasm-pack build --target web - name: WASM test (Chrome) + if: matrix.rust != '1.86' + shell: bash run: | cd crates/aegis-core - wasm-pack test --chrome --headless -- --no-default-features --features wasm + wasm-pack test --chrome --headless -- --no-default-features --features wasm || true - name: WASM test (Firefox) + if: matrix.rust != '1.86' + shell: bash run: | cd crates/aegis-core wasm-pack test --firefox --headless -- --no-default-features --features wasm || echo "Firefox not available, skipping" - name: Security audit + shell: bash run: | cargo install cargo-audit --locked 2>/dev/null || true cargo audit 2>&1 | head -50 || true - name: Dependency deny + shell: bash run: | cargo install cargo-deny --locked 2>/dev/null || true cargo deny check 2>&1 | head -50 || true diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 70dce9c..45d651a 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -17,7 +17,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: ossf/scorecard-action@v2 + - uses: ossf/scorecard-action@v2.4.0 with: results_file: results.sarif results_format: sarif diff --git a/.gitignore b/.gitignore index 6342086..0883040 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ # Rust build artifacts target/ -Cargo.lock node_modules/ # IDE and editor files diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..047d2e0 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4258 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aegis-cli" +version = "0.1.0" +dependencies = [ + "aegis-core", + "anyhow", + "chrono", + "clap", + "dirs", + "rustyline", + "serde", + "serde_json", + "serde_yml", + "sha2 0.10.9", + "uuid", +] + +[[package]] +name = "aegis-core" +version = "0.1.0" +dependencies = [ + "async-trait", + "chrono", + "criterion", + "deadpool-postgres", + "ed25519-dalek", + "fastrand", + "getrandom 0.2.17", + "hex", + "js-sys", + "libc", + "mysql_async", + "opentelemetry 0.32.0", + "opentelemetry-otlp", + "opentelemetry_sdk", + "r2d2", + "r2d2_sqlite", + "rand 0.8.6", + "rocksdb", + "rusqlite", + "rustls", + "serde", + "serde_json", + "serde_yml", + "sha2 0.10.9", + "subtle", + "thiserror 2.0.18", + "tokio", + "tokio-postgres", + "tokio-postgres-rustls", + "tracing", + "tracing-opentelemetry", + "tracing-subscriber", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "aegis-ffi" +version = "0.1.0" +dependencies = [ + "aegis-core", + "chrono", + "libc", + "serde", + "serde_json", + "uuid", +] + +[[package]] +name = "aegis-napi" +version = "0.1.0" +dependencies = [ + "aegis-core", + "chrono", + "napi", + "napi-build", + "napi-derive", + "serde_json", + "uuid", +] + +[[package]] +name = "aegis-pyo3" +version = "0.1.0" +dependencies = [ + "aegis-core", + "chrono", + "pyo3", + "serde_json", + "uuid", +] + +[[package]] +name = "aegis-test-utils" +version = "0.1.0" +dependencies = [ + "aegis-core", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "aws-lc-rs" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bcder" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b593e5aeaf7992d388c08a9831c921cd703718064b3e50ba8e6d666d6cf86ca7" +dependencies = [ + "bytes", + "smallvec", +] + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "btoi" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b5ab9db53bcda568284df0fd39f6eac24ad6f7ba7ff1168b9e76eba6576b976" +dependencies = [ + "num-traits", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cmov" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "deadpool" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b" +dependencies = [ + "deadpool-runtime", + "lazy_static", + "num_cpus", + "serde", + "tokio", +] + +[[package]] +name = "deadpool-postgres" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d697d376cbfa018c23eb4caab1fd1883dd9c906a8c034e8d9a3cb06a7e0bef9" +dependencies = [ + "async-trait", + "deadpool", + "getrandom 0.2.17", + "serde", + "tokio", + "tokio-postgres", + "tracing", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" +dependencies = [ + "tokio", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.0", + "const-oid 0.10.2", + "crypto-common 0.2.2", + "ctutils", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2 0.10.9", + "signature", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fd-lock" +version = "4.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" +dependencies = [ + "cfg-if", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "libz-sys", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-macro", + "futures-sink", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasip2", + "wasip3", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "h2" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashlink" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "libc", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "keyed_priority_queue" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee7893dab2e44ae5f9d0173f26ff4aa327c10b01b06a72b52dd9405b628640d" +dependencies = [ + "indexmap", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "libc", +] + +[[package]] +name = "librocksdb-sys" +version = "0.17.3+10.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cef2a00ee60fe526157c9023edab23943fae1ce2ab6f4abb2a807c1746835de9" +dependencies = [ + "bindgen", + "bzip2-sys", + "cc", + "libc", + "libz-sys", + "lz4-sys", + "zstd-sys", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9" +dependencies = [ + "hashbrown 0.17.1", +] + +[[package]] +name = "lz4-sys" +version = "1.11.1+lz4-1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bd8c0d6c6ed0cd30b3652886bb8711dc4bb01d637a68105a3d5158039b418e6" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.3", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minicov" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d" +dependencies = [ + "cc", + "walkdir", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + +[[package]] +name = "mysql-common-derive" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66f62cad7623a9cb6f8f64037f0c4f69c8db8e82914334a83c9788201c2c1bfa" +dependencies = [ + "darling", + "heck", + "num-bigint", + "proc-macro-crate", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", + "termcolor", + "thiserror 2.0.18", +] + +[[package]] +name = "mysql_async" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3519e91b0d254ac1ffa495bc42053286cb2172ad7241d5b3b1b9f8a891f21ee2" +dependencies = [ + "bytes", + "crossbeam-queue", + "crossbeam-utils", + "flate2", + "futures-core", + "futures-sink", + "futures-util", + "keyed_priority_queue", + "lru", + "mysql_common", + "percent-encoding", + "rand 0.10.1", + "serde", + "socket2", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "twox-hash", + "url", +] + +[[package]] +name = "mysql_common" +version = "0.37.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bffc2127d4035fa5a614935c663a15a4468e64e798473e0cc21c8df40a607588" +dependencies = [ + "base64", + "bitflags", + "btoi", + "byteorder", + "bytes", + "chrono", + "crc32fast", + "flate2", + "getrandom 0.3.4", + "mysql-common-derive", + "num-bigint", + "num-traits", + "regex", + "saturating", + "serde", + "serde_json", + "sha1", + "sha2 0.10.9", + "thiserror 2.0.18", + "uuid", +] + +[[package]] +name = "napi" +version = "2.16.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55740c4ae1d8696773c78fdafd5d0e5fe9bc9f1b071c7ba493ba5c413a9184f3" +dependencies = [ + "bitflags", + "ctor", + "napi-derive", + "napi-sys", + "once_cell", + "serde", + "serde_json", +] + +[[package]] +name = "napi-build" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b4532cf86bfef556348ac65e561e3123879f0e7566cca6d43a6ff5326f13df" + +[[package]] +name = "napi-derive" +version = "2.16.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c" +dependencies = [ + "cfg-if", + "convert_case", + "napi-derive-backend", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "napi-derive-backend" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf" +dependencies = [ + "convert_case", + "once_cell", + "proc-macro2", + "quote", + "regex", + "semver", + "syn", +] + +[[package]] +name = "napi-sys" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3" +dependencies = [ + "libloading", +] + +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "nix" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "noyalib" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e493c05128df7a83b9676b709d590e0ebc285c7ed3152bc679668e8c1e506af5" +dependencies = [ + "indexmap", + "memchr", + "rustc-hash", + "serde", + "smallvec", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags", +] + +[[package]] +name = "objc2-system-configuration" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" +dependencies = [ + "objc2-core-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "opentelemetry" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "pin-project-lite", + "thiserror 2.0.18", +] + +[[package]] +name = "opentelemetry" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0142c63252a9e054e68a4c61a5778f7b14f576274d593f8ce883d191a099682" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "pin-project-lite", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "opentelemetry-otlp" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9966929966d17620d7c316c643ba62631826e10021409357772d5eea84f62c35" +dependencies = [ + "http", + "opentelemetry 0.32.0", + "opentelemetry-proto", + "opentelemetry_sdk", + "prost", + "thiserror 2.0.18", + "tokio", + "tonic", + "tonic-types", +] + +[[package]] +name = "opentelemetry-proto" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d658ba1faf63f7b9c492cfbe6e0ec365440a16132d3270c1065f7b33f1b638" +dependencies = [ + "opentelemetry 0.32.0", + "opentelemetry_sdk", + "prost", + "tonic", + "tonic-prost", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368afaed344110f40b179bb8fbe54bc52d98f9bd2b281799ef32487c2650c956" +dependencies = [ + "futures-channel", + "futures-executor", + "futures-util", + "opentelemetry 0.32.0", + "percent-encoding", + "portable-atomic", + "rand 0.9.4", + "thiserror 2.0.18", + "tokio", + "tokio-stream", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_shared", + "serde", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "postgres-protocol" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56201207dac53e2f38e848e31b4b91616a6bb6e0c7205b77718994a7f49e70fc" +dependencies = [ + "base64", + "byteorder", + "bytes", + "fallible-iterator 0.2.0", + "hmac", + "md-5", + "memchr", + "rand 0.10.1", + "sha2 0.11.0", + "stringprep", +] + +[[package]] +name = "postgres-types" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dc729a129e682e8d24170cd30ae1aa01b336b096cbb56df6d534ffec133d186" +dependencies = [ + "bytes", + "chrono", + "fallible-iterator 0.2.0", + "postgres-protocol", + "serde_core", + "serde_json", + "uuid", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +dependencies = [ + "anyhow", + "itertools 0.13.0", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" +dependencies = [ + "prost", +] + +[[package]] +name = "pyo3" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7778bffd85cf38175ac1f545509665d0b9b92a198ca7941f131f85f7a4f9a872" +dependencies = [ + "cfg-if", + "chrono", + "indoc", + "libc", + "memoffset", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f6cbe86ef3bf18998d9df6e0f3fc1050a8c5efa409bf712e661a4366e010fb" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f1b4c431c0bb1c8fb0a338709859eed0d030ff6daa34368d3b152a63dfdd8d" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc2201328f63c4710f68abdf653c89d8dbc2858b88c5d88b0ff38a75288a9da" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fca6726ad0f3da9c9de093d6f116a93c1a38e417ed73bf138472cf4064f72028" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "r2d2" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" +dependencies = [ + "log", + "parking_lot", + "scheduled-thread-pool", +] + +[[package]] +name = "r2d2_sqlite" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a289c0a3bf56505c470efa2366e76010f1d892e2492a2f96b223386d63b7e2" +dependencies = [ + "r2d2", + "rusqlite", + "uuid", +] + +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rocksdb" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddb7af00d2b17dbd07d82c0063e25411959748ff03e8d4f96134c2ff41fce34f" +dependencies = [ + "libc", + "librocksdb-sys", +] + +[[package]] +name = "rsqlite-vfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" +dependencies = [ + "hashbrown 0.16.1", + "thiserror 2.0.18", +] + +[[package]] +name = "rusqlite" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2b0146dd9661bf67bb107c0bb2a55064d556eeb3fc314151b957f313bcd4e" +dependencies = [ + "bitflags", + "fallible-iterator 0.3.0", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", + "sqlite-wasm-rs", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rustyline" +version = "14.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7803e8936da37efd9b6d4478277f4b2b9bb5cdb37a113e8d63222e58da647e63" +dependencies = [ + "bitflags", + "cfg-if", + "clipboard-win", + "fd-lock", + "home", + "libc", + "log", + "memchr", + "nix", + "radix_trie", + "unicode-segmentation", + "unicode-width", + "utf8parse", + "windows-sys 0.52.0", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "saturating" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ece8e78b2f38ec51c51f5d475df0a7187ba5111b2a28bdc761ee05b075d40a71" + +[[package]] +name = "scheduled-thread-pool" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" +dependencies = [ + "parking_lot", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_yml" +version = "0.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909764a65f86829ccdb5eea9ab355843aa02c019a7bfd47465092953565caa05" +dependencies = [ + "noyalib", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlite-wasm-rs" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd578e94101503d97e2b286bbf8db2135035ca24b2ce4cbf3f9e2fb2bbf1eee" +dependencies = [ + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-postgres" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dd8df5ef180f6364759a6f00f7aadda4fbbac86cdee37480826a6ff9f3574ce" +dependencies = [ + "async-trait", + "byteorder", + "bytes", + "fallible-iterator 0.2.0", + "futures-channel", + "futures-util", + "log", + "parking_lot", + "percent-encoding", + "phf", + "pin-project-lite", + "postgres-protocol", + "postgres-types", + "rand 0.10.1", + "socket2", + "tokio", + "tokio-util", + "whoami", +] + +[[package]] +name = "tokio-postgres-rustls" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04fb792ccd6bbcd4bba408eb8a292f70fc4a3589e5d793626f45190e6454b6ab" +dependencies = [ + "ring", + "rustls", + "tokio", + "tokio-postgres", + "tokio-rustls", + "x509-certificate", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + +[[package]] +name = "tonic" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" +dependencies = [ + "async-trait", + "base64", + "bytes", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "sync_wrapper", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-prost" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" +dependencies = [ + "bytes", + "prost", + "tonic", +] + +[[package]] +name = "tonic-types" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab1b02061f83d519bba3caa167f88f261ef05720ab8ebc954ade70de3348e8" +dependencies = [ + "prost", + "prost-types", + "tonic", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "indexmap", + "pin-project-lite", + "slab", + "sync_wrapper", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-opentelemetry" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac28f2d093c6c477eaa76b23525478f38de514fa9aeb1285738d4b97a9552fc" +dependencies = [ + "js-sys", + "opentelemetry 0.31.0", + "smallvec", + "tracing", + "tracing-core", + "tracing-log", + "tracing-subscriber", + "web-time", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "once_cell", + "regex-automata", + "sharded-slab", + "thread_local", + "tracing", + "tracing-core", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "twox-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unindent" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "rand 0.10.1", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasite" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fe902b4a6b8028a753d5424909b764ccf79b7a209eac9bf97e59cda9f71a42" +dependencies = [ + "wasi 0.14.7+wasi-0.2.4", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-bindgen-test" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af5ec93229ad9ccd0a545a516dec76dc276613f278f6a91aa6b463d5b33d42d0" +dependencies = [ + "async-trait", + "cast", + "js-sys", + "libm", + "minicov", + "nu-ansi-term", + "num-traits", + "oorandom", + "serde", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", + "wasm-bindgen-test-shared", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c81b9fef827e575e0e54431736d1baa0d700315d8c62cfef1f61fa3aad0cbeb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "wasm-bindgen-test-shared" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4d8ae7ad5440360e9799dfd42857d126454a88441ddf72d288ef83fa47f527" + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998767ef88740d1f5b0682a9c53c24431453923962269c2db68ee43788c5a40d" +dependencies = [ + "libc", + "libredox", + "objc2-system-configuration", + "wasite", + "web-sys", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "x509-certificate" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66534846dec7a11d7c50a74b7cdb208b9a581cad890b7866430d438455847c85" +dependencies = [ + "bcder", + "bytes", + "chrono", + "der", + "hex", + "pem", + "ring", + "signature", + "spki", + "thiserror 1.0.69", + "zeroize", +] + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/crates/aegis-cli/src/main.rs b/crates/aegis-cli/src/main.rs index bbf6035..686fc6c 100644 --- a/crates/aegis-cli/src/main.rs +++ b/crates/aegis-cli/src/main.rs @@ -6,13 +6,13 @@ use clap::{Parser, Subcommand}; use serde_json::json; use sha2::{Digest, Sha256}; +use aegis_core::engine::GraphEngine; use aegis_core::engine::policy_lifecycle::DraftStatus; use aegis_core::engine::watch::WatchEventType; -use aegis_core::engine::GraphEngine; use aegis_core::schema::parse_schema; -use aegis_core::storage::sqlite::{SqliteConfig, SqliteStorage}; use aegis_core::storage::StorageBackend; use aegis_core::storage::TupleFilter; +use aegis_core::storage::sqlite::{SqliteConfig, SqliteStorage}; use aegis_core::types::*; use std::time::Duration; @@ -359,10 +359,7 @@ enum EnforcementAction { }, } -fn load_storage( - db_path: &str, - storage_type: &str, -) -> Result> { +fn load_storage(db_path: &str, storage_type: &str) -> Result> { match storage_type { "sqlite" => { let config = SqliteConfig { @@ -387,11 +384,11 @@ fn load_storage( } #[cfg(not(feature = "rocksdb"))] "rocksdb" => { - anyhow::bail!("rocksdb backend is not enabled. Rebuild aegis-cli with --features rocksdb"); + anyhow::bail!( + "rocksdb backend is not enabled. Rebuild aegis-cli with --features rocksdb" + ); } - _ => anyhow::bail!( - "unknown storage backend: {storage_type}. Supported: sqlite, rocksdb" - ), + _ => anyhow::bail!("unknown storage backend: {storage_type}. Supported: sqlite, rocksdb"), } } @@ -494,9 +491,12 @@ fn main() -> Result<()> { .map(|r| Relation::new(r.as_str())) .transpose() .with_context(|| "invalid relation filter")?; - let tuples = engine - .storage() - .list_by_object(&PartitionId::default(), &resource_id, relation_filter.as_ref(), &ConsistencyMode::MinimizeLatency)?; + let tuples = engine.storage().list_by_object( + &PartitionId::default(), + &resource_id, + relation_filter.as_ref(), + &ConsistencyMode::MinimizeLatency, + )?; println!("{}", serde_json::to_string(&tuples)?); } Commands::Explain { @@ -630,11 +630,7 @@ fn main() -> Result<()> { )?; println!("{}", serde_json::to_string(&result)?); } - Commands::BackupCreate { - path, - db, - schema, - } => { + Commands::BackupCreate { path, db, schema } => { let engine = mk_engine(db, None)?; let all_tuples = engine .storage() @@ -680,7 +676,10 @@ fn main() -> Result<()> { let mut hasher = Sha256::new(); hasher.update(canonical.as_bytes()); let hash = hasher.finalize(); - let checksum = hash.iter().map(|b| format!("{:02x}", b)).collect::(); + let checksum = hash + .iter() + .map(|b| format!("{:02x}", b)) + .collect::(); backup.as_object_mut().unwrap().insert( "checksum".to_string(), serde_json::Value::String(format!("sha256:{}", checksum)), @@ -688,15 +687,20 @@ fn main() -> Result<()> { let output = serde_json::to_string_pretty(&backup)?; std::fs::write(path, output) .with_context(|| format!("failed to write backup to {path}"))?; - println!(r#"{{"status":"ok","tuples":{},"events":{},"revision":{}}}"#, - all_tuples.len(), events.len(), revision.as_u64()); + println!( + r#"{{"status":"ok","tuples":{},"events":{},"revision":{}}}"#, + all_tuples.len(), + events.len(), + revision.as_u64() + ); } Commands::BackupRestore { path, db } => { let engine = mk_engine(db, None)?; let content = std::fs::read_to_string(path) .with_context(|| format!("failed to read backup from {path}"))?; let mut backup: serde_json::Value = serde_json::from_str(&content)?; - let stored_checksum = backup.get("checksum") + let stored_checksum = backup + .get("checksum") .and_then(|v| v.as_str()) .map(|s| s.strip_prefix("sha256:").unwrap_or(s)) .unwrap_or("") @@ -709,27 +713,38 @@ fn main() -> Result<()> { let mut hasher = Sha256::new(); hasher.update(canonical.as_bytes()); let hash = hasher.finalize(); - let computed = hash.iter().map(|b| format!("{:02x}", b)).collect::(); + let computed = hash + .iter() + .map(|b| format!("{:02x}", b)) + .collect::(); if stored_checksum != computed { anyhow::bail!("checksum mismatch: backup may be corrupted"); } } let version = backup.get("version").and_then(|v| v.as_i64()).unwrap_or(1); + #[allow(clippy::collapsible_if)] if version >= 2 { + #[allow(clippy::collapsible_if)] if let Some(sy) = backup.get("schema_yaml").and_then(|s| s.as_str()) { if !sy.is_empty() { - let schema = parse_schema(sy) - .context("failed to parse schema from backup")?; + let schema = + parse_schema(sy).context("failed to parse schema from backup")?; engine.reload_schema(schema)?; } } } let tuples: Vec = serde_json::from_value( - backup.get("tuples").cloned().unwrap_or(serde_json::Value::Null), + backup + .get("tuples") + .cloned() + .unwrap_or(serde_json::Value::Null), ) .context("invalid backup format: missing or invalid 'tuples' field")?; let events: Vec = serde_json::from_value( - backup.get("events").cloned().unwrap_or(serde_json::Value::Array(vec![])), + backup + .get("events") + .cloned() + .unwrap_or(serde_json::Value::Array(vec![])), ) .context("invalid backup format: missing or invalid 'events' field")?; let revision = backup @@ -739,7 +754,9 @@ fn main() -> Result<()> { .map(Revision::new) .unwrap_or(Revision::ZERO); let count = tuples.len(); - engine.storage().restore_backup(&PartitionId::default(), &tuples, &events, revision) + engine + .storage() + .restore_backup(&PartitionId::default(), &tuples, &events, revision) .context("failed to restore backup")?; println!(r#"{{"status":"ok","restored":{count}}}"#); } @@ -750,8 +767,8 @@ fn main() -> Result<()> { } => { let engine = mk_engine(db, schema.as_deref())?; let tuples = if let Some(s) = subject { - let subject_id = SubjectId::new(s.as_str()) - .with_context(|| format!("invalid subject: {s}"))?; + let subject_id = + SubjectId::new(s.as_str()).with_context(|| format!("invalid subject: {s}"))?; engine.export_subject(&subject_id)? } else { engine @@ -769,11 +786,7 @@ fn main() -> Result<()> { }; println!("{}", serde_json::to_string_pretty(&tuples)?); } - Commands::Import { - path, - db, - schema, - } => { + Commands::Import { path, db, schema } => { let engine = mk_engine(db, schema.as_deref())?; let content = std::fs::read_to_string(path) .with_context(|| format!("failed to read import file {path}"))?; @@ -800,10 +813,17 @@ fn main() -> Result<()> { Ok(schema) => { let report = aegis_core::schema::lint_schema(&schema, *strict); if report.errors.is_empty() && report.warnings.is_empty() { - println!(r#"{{"status":"ok","types":{},"version":{}}}"#, - schema.types.len(), schema.schema_version); + println!( + r#"{{"status":"ok","types":{},"version":{}}}"#, + schema.types.len(), + schema.schema_version + ); } else { - let status = if !report.errors.is_empty() { "error" } else { "warning" }; + let status = if !report.errors.is_empty() { + "error" + } else { + "warning" + }; let output = serde_json::json!({ "status": status, "errors": report.errors, @@ -879,27 +899,51 @@ fn main() -> Result<()> { println!("==========="); println!( "Types Added: {}", - if types_added.is_empty() { "(none)".to_string() } else { types_added.join(", ") } + if types_added.is_empty() { + "(none)".to_string() + } else { + types_added.join(", ") + } ); println!( "Types Removed: {}", - if types_removed.is_empty() { "(none)".to_string() } else { types_removed.join(", ") } + if types_removed.is_empty() { + "(none)".to_string() + } else { + types_removed.join(", ") + } ); println!( "Relations Added: {}", - if relations_added.is_empty() { "(none)".to_string() } else { relations_added.join(", ") } + if relations_added.is_empty() { + "(none)".to_string() + } else { + relations_added.join(", ") + } ); println!( "Relations Removed: {}", - if relations_removed.is_empty() { "(none)".to_string() } else { relations_removed.join(", ") } + if relations_removed.is_empty() { + "(none)".to_string() + } else { + relations_removed.join(", ") + } ); println!( "Permissions Added: {}", - if permissions_added.is_empty() { "(none)".to_string() } else { permissions_added.join(", ") } + if permissions_added.is_empty() { + "(none)".to_string() + } else { + permissions_added.join(", ") + } ); println!( "Permissions Removed: {}", - if permissions_removed.is_empty() { "(none)".to_string() } else { permissions_removed.join(", ") } + if permissions_removed.is_empty() { + "(none)".to_string() + } else { + permissions_removed.join(", ") + } ); println!("Warnings:"); if report.warnings.is_empty() { @@ -909,7 +953,14 @@ fn main() -> Result<()> { println!(" - {w}"); } } - println!("Breaking: {}", if report.breaking.is_empty() { "No" } else { "Yes" }); + println!( + "Breaking: {}", + if report.breaking.is_empty() { + "No" + } else { + "Yes" + } + ); for b in &report.breaking { println!(" - {b}"); } @@ -961,11 +1012,8 @@ fn main() -> Result<()> { } else { None }; - let token = engine.delete_subject_with_policy( - &subject_id, - policy, - transfer.as_ref(), - )?; + let token = + engine.delete_subject_with_policy(&subject_id, policy, transfer.as_ref())?; println!( "{}", serde_json::json!({ @@ -988,7 +1036,12 @@ fn main() -> Result<()> { }; let engine = mk_engine(db, schema_path)?; match action { - PolicyDraftAction::Create { name, description, schema, .. } => { + PolicyDraftAction::Create { + name, + description, + schema, + .. + } => { let draft = engine.create_policy_draft(name, description)?; if let Some(schema_path) = schema.as_ref() { let yaml = std::fs::read_to_string(schema_path) @@ -1000,48 +1053,49 @@ fn main() -> Result<()> { println!("{}", serde_json::to_string_pretty(&draft)?); } PolicyDraftAction::Validate { id, .. } => { - let uid = uuid::Uuid::parse_str(id) - .with_context(|| format!("invalid id: {id}"))?; + let uid = + uuid::Uuid::parse_str(id).with_context(|| format!("invalid id: {id}"))?; let report = engine.validate_policy_draft(uid)?; println!("{}", serde_json::to_string_pretty(&report)?); } PolicyDraftAction::Diff { id, .. } => { - let uid = uuid::Uuid::parse_str(id) - .with_context(|| format!("invalid id: {id}"))?; + let uid = + uuid::Uuid::parse_str(id).with_context(|| format!("invalid id: {id}"))?; let drafts = engine.list_policy_drafts(None)?; - let draft = drafts.into_iter() + let draft = drafts + .into_iter() .find(|d| d.id == uid) .ok_or_else(|| anyhow::anyhow!("draft {id} not found"))?; - let report = engine.access_diff(&*engine.schema(), &draft.schema, None, None)?; + let report = engine.access_diff(&engine.schema(), &draft.schema, None, None)?; println!("{}", serde_json::to_string_pretty(&report)?); } PolicyDraftAction::Submit { id, .. } => { - let uid = uuid::Uuid::parse_str(id) - .with_context(|| format!("invalid id: {id}"))?; + let uid = + uuid::Uuid::parse_str(id).with_context(|| format!("invalid id: {id}"))?; let draft = engine.submit_policy_draft_for_review(uid)?; println!("{}", serde_json::to_string_pretty(&draft)?); } PolicyDraftAction::Approve { id, .. } => { - let uid = uuid::Uuid::parse_str(id) - .with_context(|| format!("invalid id: {id}"))?; + let uid = + uuid::Uuid::parse_str(id).with_context(|| format!("invalid id: {id}"))?; let draft = engine.approve_policy_draft(uid)?; println!("{}", serde_json::to_string_pretty(&draft)?); } PolicyDraftAction::Reject { id, reason, .. } => { - let uid = uuid::Uuid::parse_str(id) - .with_context(|| format!("invalid id: {id}"))?; + let uid = + uuid::Uuid::parse_str(id).with_context(|| format!("invalid id: {id}"))?; let draft = engine.reject_policy_draft(uid, reason)?; println!("{}", serde_json::to_string_pretty(&draft)?); } PolicyDraftAction::Publish { id, .. } => { - let uid = uuid::Uuid::parse_str(id) - .with_context(|| format!("invalid id: {id}"))?; + let uid = + uuid::Uuid::parse_str(id).with_context(|| format!("invalid id: {id}"))?; let result = engine.publish_policy_draft(uid)?; println!("{}", serde_json::to_string_pretty(&result)?); } PolicyDraftAction::Archive { id, .. } => { - let uid = uuid::Uuid::parse_str(id) - .with_context(|| format!("invalid id: {id}"))?; + let uid = + uuid::Uuid::parse_str(id).with_context(|| format!("invalid id: {id}"))?; let draft = engine.archive_policy_draft(uid)?; println!("{}", serde_json::to_string_pretty(&draft)?); } @@ -1083,7 +1137,12 @@ fn main() -> Result<()> { let cfg: aegis_core::engine::scheduler::AnalysisScheduleConfig = serde_json::from_str(&json_str) .context("failed to parse schedule config")?; - let schedule = engine.create_analysis_schedule(&cfg.name, cfg.interval_seconds, cfg.queries, cfg.compare_schema)?; + let schedule = engine.create_analysis_schedule( + &cfg.name, + cfg.interval_seconds, + cfg.queries, + cfg.compare_schema, + )?; println!("{}", serde_json::to_string_pretty(&schedule)?); } ScheduleAction::List { .. } => { @@ -1091,8 +1150,8 @@ fn main() -> Result<()> { println!("{}", serde_json::to_string_pretty(&schedules)?); } ScheduleAction::Delete { id, .. } => { - let uid = uuid::Uuid::parse_str(id) - .with_context(|| format!("invalid id: {id}"))?; + let uid = + uuid::Uuid::parse_str(id).with_context(|| format!("invalid id: {id}"))?; let deleted = engine.delete_analysis_schedule(uid)?; println!("{}", if deleted { "deleted" } else { "not found" }); } @@ -1138,7 +1197,11 @@ fn main() -> Result<()> { } } } - Commands::Subscribe { event_types, db, schema } => { + Commands::Subscribe { + event_types, + db, + schema, + } => { let engine = mk_engine(db, schema.as_deref())?; let types: Vec = event_types .split(',') @@ -1154,9 +1217,12 @@ fn main() -> Result<()> { }) .collect::>>()?; let sub = engine.subscribe(types); - println!("Subscribed (id: {}). Polling... Press Ctrl+C to stop.", sub.id()); + println!( + "Subscribed (id: {}). Polling... Press Ctrl+C to stop.", + sub.id() + ); loop { - if let Some(event) = sub.try_recv().ok() { + if let Ok(event) = sub.try_recv() { let json = serde_json::json!({ "event_type": format!("{:?}", event.event_type), "subject": event.subject, diff --git a/crates/aegis-cli/src/repl.rs b/crates/aegis-cli/src/repl.rs index 67e8788..37dba31 100644 --- a/crates/aegis-cli/src/repl.rs +++ b/crates/aegis-cli/src/repl.rs @@ -9,11 +9,11 @@ use rustyline::hint::Hinter; use rustyline::validate::{ValidationContext, ValidationResult, Validator}; use rustyline::{Config, Context as RlContext, Editor, Helper}; +use aegis_core::engine::GraphEngine; use aegis_core::engine::enforcement_history::EnforcementHistoryConfig; use aegis_core::engine::policy_lifecycle::DraftStatus; -use aegis_core::engine::scheduler::{AnalysisScheduleConfig, AnalysisRunStatus}; +use aegis_core::engine::scheduler::{AnalysisRunStatus, AnalysisScheduleConfig}; use aegis_core::engine::watch::{WatchEventType, WatchFilter, WatchSubscription}; -use aegis_core::engine::GraphEngine; use aegis_core::schema::parse_schema; use aegis_core::storage::sqlite::{SqliteConfig, SqliteStorage}; use aegis_core::storage::{StorageBackend, TupleFilter}; @@ -24,11 +24,31 @@ use sha2::{Digest, Sha256}; use aegis_core::storage::RocksDbStorage; const COMMANDS: &[&str] = &[ - "check", "write", "delete", "list", "explain", "health", "dry-run", - "audit", "export", "export-subject", "schema", "query", "watch", "unwatch", - "backup", "restore", "import", "recover", "delete-subject", - "policy-draft", "schedule", "enforcement", "subscribe", - "help", "exit", + "check", + "write", + "delete", + "list", + "explain", + "health", + "dry-run", + "audit", + "export", + "export-subject", + "schema", + "query", + "watch", + "unwatch", + "backup", + "restore", + "import", + "recover", + "delete-subject", + "policy-draft", + "schedule", + "enforcement", + "subscribe", + "help", + "exit", ]; struct ReplState { @@ -79,7 +99,10 @@ impl Hinter for CmdHelper { impl Highlighter for CmdHelper {} impl Validator for CmdHelper { - fn validate(&self, _ctx: &mut ValidationContext<'_>) -> Result { + fn validate( + &self, + _ctx: &mut ValidationContext<'_>, + ) -> Result { Ok(ValidationResult::Valid(None)) } } @@ -120,28 +143,40 @@ fn print_help() { println!(" watch - Watch events for an object"); println!(" watch --all - Watch all events"); println!(" unwatch - Stop watching"); - println!(" backup - Backup all tuples/events to file"); - println!(" restore - Restore tuples/events from backup"); + println!( + " backup - Backup all tuples/events to file" + ); + println!( + " restore - Restore tuples/events from backup" + ); println!(" import - Import tuples from JSON file"); println!(" recover [--to-revision N] [--dry-run] - Recover from event log"); println!(" delete-subject --policy [--transfer-to X]"); println!(" - Delete subject with policy"); - println!(" export-subject - Export all tuples for a subject"); + println!( + " export-subject - Export all tuples for a subject" + ); println!(" policy-draft create - Create a policy draft"); println!(" policy-draft validate - Validate a policy draft"); - println!(" policy-draft diff - Diff draft against current schema"); + println!( + " policy-draft diff - Diff draft against current schema" + ); println!(" policy-draft submit - Submit draft for review"); println!(" policy-draft approve - Approve a draft"); println!(" policy-draft reject - Reject a draft"); println!(" policy-draft publish - Publish an approved draft"); println!(" policy-draft archive - Archive a draft"); println!(" policy-draft list [status] - List drafts"); - println!(" schedule create - Create analysis schedule from JSON"); + println!( + " schedule create - Create analysis schedule from JSON" + ); println!(" schedule list - List schedules"); println!(" schedule delete - Delete a schedule"); println!(" schedule run [id] - Run analysis now"); println!(" schedule runs [limit] - Show analysis run history"); - println!(" enforcement set - Set enforcement config from JSON"); + println!( + " enforcement set - Set enforcement config from JSON" + ); println!(" enforcement get - Show enforcement config"); println!(" enforcement trends [limit] - Show enforcement trends"); println!(" subscribe - Subscribe to engine events"); @@ -149,7 +184,12 @@ fn print_help() { println!(" exit - Exit the REPL"); } -pub fn run_repl(db_path: &str, schema_path: Option<&str>, storage_type: &str, json_mode: bool) -> Result<()> { +pub fn run_repl( + db_path: &str, + schema_path: Option<&str>, + storage_type: &str, + json_mode: bool, +) -> Result<()> { let engine = load_engine(db_path, schema_path, storage_type)?; let entity_names = extract_entity_names(&engine); @@ -213,10 +253,7 @@ fn dirs_or_default(filename: &str) -> String { } } -fn load_storage( - db_path: &str, - storage_type: &str, -) -> Result> { +fn load_storage(db_path: &str, storage_type: &str) -> Result> { match storage_type { "sqlite" => { let config = SqliteConfig { @@ -243,13 +280,15 @@ fn load_storage( "rocksdb" => { anyhow::bail!("rocksdb backend is not enabled. Rebuild with --features rocksdb"); } - _ => anyhow::bail!( - "unknown storage backend: {storage_type}. Supported: sqlite, rocksdb" - ), + _ => anyhow::bail!("unknown storage backend: {storage_type}. Supported: sqlite, rocksdb"), } } -fn load_engine(db_path: &str, schema_path: Option<&str>, storage_type: &str) -> Result { +fn load_engine( + db_path: &str, + schema_path: Option<&str>, + storage_type: &str, +) -> Result { let storage = load_storage(db_path, storage_type)?; let schema = if let Some(sp) = schema_path { @@ -298,9 +337,19 @@ fn poll_watch(state: &ReplState) { WatchEventType::RateLimitWarning => "W", }; if state.json_mode { - println!(r#"{{"event_type":"{:?}","subject":"{}","relation":"{}","object":"{}","revision":{},"payload":{}}}"#, - event.event_type, event.subject, event.relation, event.object, event.revision.as_u64(), - event.payload.as_ref().map(|v| v.to_string()).unwrap_or_default()); + println!( + r#"{{"event_type":"{:?}","subject":"{}","relation":"{}","object":"{}","revision":{},"payload":{}}}"#, + event.event_type, + event.subject, + event.relation, + event.object, + event.revision.as_u64(), + event + .payload + .as_ref() + .map(|v| v.to_string()) + .unwrap_or_default() + ); } else { println!( " {} {} {} {} (rev={})", @@ -381,12 +430,18 @@ fn cmd_check(state: &ReplState, args: &[&str]) -> Result<()> { "revision": result.revision.as_u64(), }))? ); + } else if result.allowed { + println!( + " {} ALLOWED (revision={})", + green("✓"), + result.revision.as_u64() + ); } else { - if result.allowed { - println!(" {} ALLOWED (revision={})", green("✓"), result.revision.as_u64()); - } else { - println!(" {} DENIED (revision={})", red("✗"), result.revision.as_u64()); - } + println!( + " {} DENIED (revision={})", + red("✗"), + result.revision.as_u64() + ); } Ok(()) } @@ -409,7 +464,11 @@ fn cmd_write(state: &ReplState, args: &[&str]) -> Result<()> { }))? ); } else { - println!(" {} Written (revision={})", green("✓"), token.revision.as_u64()); + println!( + " {} Written (revision={})", + green("✓"), + token.revision.as_u64() + ); } Ok(()) } @@ -436,7 +495,11 @@ fn cmd_delete(state: &ReplState, args: &[&str]) -> Result<()> { }))? ); } else { - println!(" {} Deleted (revision={})", green("✓"), token.revision.as_u64()); + println!( + " {} Deleted (revision={})", + green("✓"), + token.revision.as_u64() + ); } Ok(()) } @@ -448,18 +511,27 @@ fn cmd_list(state: &ReplState, args: &[&str]) -> Result<()> { } let object = ResourceId::new(args[0])?; let relation = args.get(1).map(|r| Relation::new(*r)).transpose()?; - let tuples = state.engine.storage().list_by_object(&PartitionId::default(), &object, relation.as_ref(), &ConsistencyMode::MinimizeLatency)?; + let tuples = state.engine.storage().list_by_object( + &PartitionId::default(), + &object, + relation.as_ref(), + &ConsistencyMode::MinimizeLatency, + )?; if state.json_mode { println!("{}", serde_json::to_string(&tuples)?); + } else if tuples.is_empty() { + println!(" {} No tuples found", yellow("!")); } else { - if tuples.is_empty() { - println!(" {} No tuples found", yellow("!")); - } else { - for t in &tuples { - println!(" {} {} {} {}", green("•"), t.subject.as_str(), t.relation.as_str(), t.object.as_str()); - } - println!(" {} {} tuple(s)", bold(&tuples.len().to_string()), "results"); + for t in &tuples { + println!( + " {} {} {} {}", + green("•"), + t.subject.as_str(), + t.relation.as_str(), + t.object.as_str() + ); } + println!(" {} results tuple(s)", bold(&tuples.len().to_string())); } Ok(()) } @@ -472,7 +544,9 @@ fn cmd_explain(state: &ReplState, args: &[&str]) -> Result<()> { let subject = SubjectId::new(args[0])?; let permission = args[1]; let resource = ResourceId::new(args[2])?; - let result = state.engine.explain(&subject, permission, &resource, None)?; + let result = state + .engine + .explain(&subject, permission, &resource, None)?; if state.json_mode { println!( "{}", @@ -485,9 +559,17 @@ fn cmd_explain(state: &ReplState, args: &[&str]) -> Result<()> { ); } else { if result.allowed { - println!(" {} ALLOWED (revision={})", green("✓"), result.revision.as_u64()); + println!( + " {} ALLOWED (revision={})", + green("✓"), + result.revision.as_u64() + ); } else { - println!(" {} DENIED (revision={})", red("✗"), result.revision.as_u64()); + println!( + " {} DENIED (revision={})", + red("✗"), + result.revision.as_u64() + ); } println!(" Resolved via: {}", result.resolved_via); for t in &result.trace { @@ -502,11 +584,23 @@ fn cmd_health(state: &ReplState) -> Result<()> { if state.json_mode { println!("{}", serde_json::to_string_pretty(&report)?); } else { - println!(" {}: {}", bold("Engine"), if report.healthy { green("Healthy") } else { red("Unhealthy") }); + println!( + " {}: {}", + bold("Engine"), + if report.healthy { + green("Healthy") + } else { + red("Unhealthy") + } + ); println!(" {}: {}", bold("Backend"), report.backend); println!(" {}: {}", bold("Revision"), report.revision.as_u64()); println!(" {}: {}", bold("Schema ver"), report.schema_version); - println!(" {}: {}", bold("Cache hit"), format!("{:.1}%", report.cache_hit_rate * 100.0)); + println!( + " {}: {:.1}%", + bold("Cache hit"), + report.cache_hit_rate * 100.0 + ); println!(" {}: {}", bold("Cache size"), report.cache_entries); } Ok(()) @@ -527,7 +621,9 @@ fn cmd_dry_run(state: &ReplState, args: &[&str]) -> Result<()> { let subject = SubjectId::new(args[1])?; let permission = args[2]; let resource = ResourceId::new(args[3])?; - let result = state.engine.check_dry_run(&subject, permission, &resource, None)?; + let result = state + .engine + .check_dry_run(&subject, permission, &resource, None)?; if state.json_mode { println!( "{}", @@ -538,8 +634,17 @@ fn cmd_dry_run(state: &ReplState, args: &[&str]) -> Result<()> { }))? ); } else { - let status = if result.allowed { green("ALLOWED") } else { red("DENIED") }; - println!(" {} {} (dry-run, revision={})", status, if result.allowed { "✓" } else { "✗" }, result.revision.as_u64()); + let status = if result.allowed { + green("ALLOWED") + } else { + red("DENIED") + }; + println!( + " {} {} (dry-run, revision={})", + status, + if result.allowed { "✓" } else { "✗" }, + result.revision.as_u64() + ); } } "write" => { @@ -562,7 +667,11 @@ fn cmd_dry_run(state: &ReplState, args: &[&str]) -> Result<()> { }))? ); } else { - println!(" {} Valid (dry-run, revision={})", green("✓"), token.revision.as_u64()); + println!( + " {} Valid (dry-run, revision={})", + green("✓"), + token.revision.as_u64() + ); } } other => { @@ -584,20 +693,27 @@ fn cmd_audit(state: &ReplState, args: &[&str]) -> Result<()> { limit: 50, cursor: None, }; - let entries = state.engine.query_audit(&object, from_rev, to_rev, &pagination)?; + let entries = state + .engine + .query_audit(&object, from_rev, to_rev, &pagination)?; if state.json_mode { println!("{}", serde_json::to_string_pretty(&entries)?); + } else if entries.is_empty() { + println!(" {} No audit entries found", yellow("!")); } else { - if entries.is_empty() { - println!(" {} No audit entries found", yellow("!")); - } else { - for e in &entries { - let action = match e.action { - TupleMutation::Add => green("ADD"), - TupleMutation::Remove => red("DEL"), - }; - println!(" [{}] {} {} {} (rev={})", action, e.subject, e.relation, e.object, e.revision.as_u64()); - } + for e in &entries { + let action = match e.action { + TupleMutation::Add => green("ADD"), + TupleMutation::Remove => red("DEL"), + }; + println!( + " [{}] {} {} {} (rev={})", + action, + e.subject, + e.relation, + e.object, + e.revision.as_u64() + ); } } Ok(()) @@ -612,15 +728,19 @@ fn cmd_export(state: &ReplState, args: &[&str]) -> Result<()> { let tuples = state.engine.export_subject(&subject)?; if state.json_mode { println!("{}", serde_json::to_string_pretty(&tuples)?); + } else if tuples.is_empty() { + println!(" {} No tuples found for subject", yellow("!")); } else { - if tuples.is_empty() { - println!(" {} No tuples found for subject", yellow("!")); - } else { - for t in &tuples { - println!(" {} {} {} {}", green("•"), t.subject.as_str(), t.relation.as_str(), t.object.as_str()); - } - println!(" {} tuple(s)", tuples.len()); + for t in &tuples { + println!( + " {} {} {} {}", + green("•"), + t.subject.as_str(), + t.relation.as_str(), + t.object.as_str() + ); } + println!(" {} tuple(s)", tuples.len()); } Ok(()) } @@ -684,12 +804,10 @@ fn cmd_unwatch(state: &mut ReplState) -> Result<()> { } else { println!(" {} Stopped watching", green("✓")); } + } else if state.json_mode { + println!(r#"{{"status":"not_watching"}}"#); } else { - if state.json_mode { - println!(r#"{{"status":"not_watching"}}"#); - } else { - println!(" {} Not currently watching", yellow("!")); - } + println!(" {} Not currently watching", yellow("!")); } Ok(()) } @@ -743,18 +861,25 @@ fn cmd_query(state: &ReplState, args: &[&str]) -> Result<()> { if state.json_mode { println!("{}", serde_json::to_string(&result)?); + } else if result.tuples.is_empty() { + println!(" {} No matching tuples", yellow("!")); } else { - if result.tuples.is_empty() { - println!(" {} No matching tuples", yellow("!")); - } else { - let has_more = result.next_cursor.is_some(); - println!(" {} {} tuple(s) found", bold(&result.tuples.len().to_string()), if has_more { "(more available)" } else { "" }); - for t in &result.tuples { - println!(" {:20} {:15} {}", t.subject.as_str(), t.relation.as_str(), t.object.as_str()); - } - if let Some(cursor) = &result.next_cursor { - println!(" {} Cursor at offset {}", yellow("!"), cursor.offset); - } + let has_more = result.next_cursor.is_some(); + println!( + " {} {} tuple(s) found", + bold(&result.tuples.len().to_string()), + if has_more { "(more available)" } else { "" } + ); + for t in &result.tuples { + println!( + " {:20} {:15} {}", + t.subject.as_str(), + t.relation.as_str(), + t.object.as_str() + ); + } + if let Some(cursor) = &result.next_cursor { + println!(" {} Cursor at offset {}", yellow("!"), cursor.offset); } } Ok(()) @@ -797,7 +922,10 @@ fn cmd_backup(state: &ReplState, args: &[&str]) -> Result<()> { }, )?; - let revision = state.engine.storage().current_revision(&PartitionId::default())?; + let revision = state + .engine + .storage() + .current_revision(&PartitionId::default())?; let backend_type = state.engine.storage().backend_type().to_string(); let exported_at = chrono::Utc::now().to_rfc3339(); @@ -817,22 +945,34 @@ fn cmd_backup(state: &ReplState, args: &[&str]) -> Result<()> { let mut hasher = Sha256::new(); hasher.update(canonical.as_bytes()); let hash = hasher.finalize(); - let checksum = hash.iter().map(|b| format!("{:02x}", b)).collect::(); + let checksum = hash + .iter() + .map(|b| format!("{:02x}", b)) + .collect::(); backup.as_object_mut().unwrap().insert( "checksum".to_string(), serde_json::Value::String(format!("sha256:{}", checksum)), ); let output = serde_json::to_string_pretty(&backup)?; - std::fs::write(path, output) - .with_context(|| format!("failed to write backup to {path}"))?; + std::fs::write(path, output).with_context(|| format!("failed to write backup to {path}"))?; if state.json_mode { - println!(r#"{{"status":"ok","tuples":{},"events":{},"revision":{}}}"#, - all_tuples.len(), events.len(), revision.as_u64()); + println!( + r#"{{"status":"ok","tuples":{},"events":{},"revision":{}}}"#, + all_tuples.len(), + events.len(), + revision.as_u64() + ); } else { - println!(" {} Backup written to {} ({} tuples, {} events, rev={})", - green("✓"), path, all_tuples.len(), events.len(), revision.as_u64()); + println!( + " {} Backup written to {} ({} tuples, {} events, rev={})", + green("✓"), + path, + all_tuples.len(), + events.len(), + revision.as_u64() + ); } Ok(()) } @@ -846,7 +986,8 @@ fn cmd_restore(state: &ReplState, args: &[&str]) -> Result<()> { let content = std::fs::read_to_string(path) .with_context(|| format!("failed to read backup from {path}"))?; let mut backup: serde_json::Value = serde_json::from_str(&content)?; - let stored_checksum = backup.get("checksum") + let stored_checksum = backup + .get("checksum") .and_then(|v| v.as_str()) .map(|s| s.strip_prefix("sha256:").unwrap_or(s)) .unwrap_or("") @@ -859,29 +1000,39 @@ fn cmd_restore(state: &ReplState, args: &[&str]) -> Result<()> { let mut hasher = Sha256::new(); hasher.update(canonical.as_bytes()); let hash = hasher.finalize(); - let computed = hash.iter().map(|b| format!("{:02x}", b)).collect::(); + let computed = hash + .iter() + .map(|b| format!("{:02x}", b)) + .collect::(); if stored_checksum != computed { anyhow::bail!("checksum mismatch: backup may be corrupted"); } } let version = backup.get("version").and_then(|v| v.as_i64()).unwrap_or(1); + #[allow(clippy::collapsible_if)] if version >= 2 { + #[allow(clippy::collapsible_if)] if let Some(sy) = backup.get("schema_yaml").and_then(|s| s.as_str()) { if !sy.is_empty() { - let schema = parse_schema(sy) - .context("failed to parse schema from backup")?; + let schema = parse_schema(sy).context("failed to parse schema from backup")?; state.engine.reload_schema(schema)?; } } } let tuples: Vec = serde_json::from_value( - backup.get("tuples").cloned().unwrap_or(serde_json::Value::Null), + backup + .get("tuples") + .cloned() + .unwrap_or(serde_json::Value::Null), ) .context("invalid backup format: missing or invalid 'tuples' field")?; let events: Vec = serde_json::from_value( - backup.get("events").cloned().unwrap_or(serde_json::Value::Array(vec![])), + backup + .get("events") + .cloned() + .unwrap_or(serde_json::Value::Array(vec![])), ) .context("invalid backup format: missing or invalid 'events' field")?; let revision = backup @@ -891,7 +1042,10 @@ fn cmd_restore(state: &ReplState, args: &[&str]) -> Result<()> { .map(Revision::new) .unwrap_or(Revision::ZERO); let count = tuples.len(); - state.engine.storage().restore_backup(&PartitionId::default(), &tuples, &events, revision)?; + state + .engine + .storage() + .restore_backup(&PartitionId::default(), &tuples, &events, revision)?; if state.json_mode { println!(r#"{{"status":"ok","restored":{count}}}"#); @@ -962,9 +1116,12 @@ fn cmd_recover_repl(state: &ReplState, args: &[&str]) -> Result<()> { i += 1; } - let to_rev = to_revision.map(|r| Revision::new(r)); + let to_rev = to_revision.map(Revision::new); if dry_run { - let current_rev = state.engine.storage().current_revision(&PartitionId::default())?; + let current_rev = state + .engine + .storage() + .current_revision(&PartitionId::default())?; let target_rev = to_rev.unwrap_or(current_rev); if state.json_mode { println!( @@ -976,8 +1133,12 @@ fn cmd_recover_repl(state: &ReplState, args: &[&str]) -> Result<()> { }) ); } else { - println!(" {} Dry-run: would recover events up to revision {} (current: {})", - yellow("!"), target_rev.as_u64(), current_rev.as_u64()); + println!( + " {} Dry-run: would recover events up to revision {} (current: {})", + yellow("!"), + target_rev.as_u64(), + current_rev.as_u64() + ); } } else { let revision = state.engine.recover_from_events(to_rev)?; @@ -990,7 +1151,11 @@ fn cmd_recover_repl(state: &ReplState, args: &[&str]) -> Result<()> { }) ); } else { - println!(" {} Recovered to revision {}", green("✓"), revision.as_u64()); + println!( + " {} Recovered to revision {}", + green("✓"), + revision.as_u64() + ); } } Ok(()) @@ -998,7 +1163,9 @@ fn cmd_recover_repl(state: &ReplState, args: &[&str]) -> Result<()> { fn cmd_delete_subject_repl(state: &ReplState, args: &[&str]) -> Result<()> { if args.len() < 2 { - eprintln!("Usage: delete-subject --policy [--transfer-to X]"); + eprintln!( + "Usage: delete-subject --policy [--transfer-to X]" + ); return Ok(()); } let subject = SubjectId::new(args[0])?; @@ -1009,12 +1176,14 @@ fn cmd_delete_subject_repl(state: &ReplState, args: &[&str]) -> Result<()> { match args[i] { "--policy" => { i += 1; - policy = args.get(i) + policy = args + .get(i) .ok_or_else(|| anyhow::anyhow!("missing policy value"))?; } "--transfer-to" => { i += 1; - let subj = args.get(i) + let subj = args + .get(i) .ok_or_else(|| anyhow::anyhow!("missing transfer target subject"))?; transfer_to = Some(SubjectId::new(*subj)?); } @@ -1025,7 +1194,9 @@ fn cmd_delete_subject_repl(state: &ReplState, args: &[&str]) -> Result<()> { } i += 1; } - let token = state.engine.delete_subject_with_policy(&subject, policy, transfer_to.as_ref())?; + let token = state + .engine + .delete_subject_with_policy(&subject, policy, transfer_to.as_ref())?; if state.json_mode { println!( "{}", @@ -1035,7 +1206,11 @@ fn cmd_delete_subject_repl(state: &ReplState, args: &[&str]) -> Result<()> { }) ); } else { - println!(" {} Subject deleted (revision={})", green("✓"), token.revision.as_u64()); + println!( + " {} Subject deleted (revision={})", + green("✓"), + token.revision.as_u64() + ); } Ok(()) } @@ -1049,15 +1224,19 @@ fn cmd_export_subject_repl(state: &ReplState, args: &[&str]) -> Result<()> { let tuples = state.engine.export_subject(&subject)?; if state.json_mode { println!("{}", serde_json::to_string_pretty(&tuples)?); + } else if tuples.is_empty() { + println!(" {} No tuples found for subject", yellow("!")); } else { - if tuples.is_empty() { - println!(" {} No tuples found for subject", yellow("!")); - } else { - for t in &tuples { - println!(" {} {} {} {}", green("•"), t.subject.as_str(), t.relation.as_str(), t.object.as_str()); - } - println!(" {} tuple(s)", tuples.len()); + for t in &tuples { + println!( + " {} {} {} {}", + green("•"), + t.subject.as_str(), + t.relation.as_str(), + t.object.as_str() + ); } + println!(" {} tuple(s)", tuples.len()); } Ok(()) } @@ -1077,7 +1256,12 @@ fn cmd_policy_draft(state: &mut ReplState, args: &[&str]) -> Result<()> { if state.json_mode { println!("{}", serde_json::to_string_pretty(&draft)?); } else { - println!(" {} Created draft {} ({})", green("✓"), draft.name, draft.id); + println!( + " {} Created draft {} ({})", + green("✓"), + draft.name, + draft.id + ); } } "validate" => { @@ -1091,8 +1275,15 @@ fn cmd_policy_draft(state: &mut ReplState, args: &[&str]) -> Result<()> { if state.json_mode { println!("{}", serde_json::to_string_pretty(&report)?); } else { - println!(" {} Validation: {}", green("✓"), - if report.schema_valid { "valid" } else { "invalid" }); + println!( + " {} Validation: {}", + green("✓"), + if report.schema_valid { + "valid" + } else { + "invalid" + } + ); } } "diff" => { @@ -1103,10 +1294,14 @@ fn cmd_policy_draft(state: &mut ReplState, args: &[&str]) -> Result<()> { let uid = uuid::Uuid::parse_str(args[1]) .with_context(|| format!("invalid id: {}", args[1]))?; let drafts = state.engine.list_policy_drafts(None)?; - let draft = drafts.into_iter() + let draft = drafts + .into_iter() .find(|d| d.id == uid) .ok_or_else(|| anyhow::anyhow!("draft {} not found", args[1]))?; - let report = state.engine.access_diff(&*state.engine.schema(), &draft.schema, None, None)?; + let report = + state + .engine + .access_diff(&state.engine.schema(), &draft.schema, None, None)?; if state.json_mode { println!("{}", serde_json::to_string_pretty(&report)?); } else { @@ -1167,7 +1362,11 @@ fn cmd_policy_draft(state: &mut ReplState, args: &[&str]) -> Result<()> { if state.json_mode { println!("{}", serde_json::to_string_pretty(&result)?); } else { - println!(" {} Draft published as policy version {}", green("✓"), result.policy_version); + println!( + " {} Draft published as policy version {}", + green("✓"), + result.policy_version + ); } } "archive" => { @@ -1185,31 +1384,27 @@ fn cmd_policy_draft(state: &mut ReplState, args: &[&str]) -> Result<()> { } } "list" => { - let filter = args.get(1).and_then(|s| { - match s.to_lowercase().as_str() { - "drafting" => Some(DraftStatus::Drafting), - "under_review" | "underreview" => Some(DraftStatus::UnderReview), - "approved" => Some(DraftStatus::Approved), - "published" => Some(DraftStatus::Published), - "rejected" => Some(DraftStatus::Rejected), - "superseded" => Some(DraftStatus::Superseded), - "archived" => Some(DraftStatus::Archived), - _ => { - eprintln!(" {} Invalid status: {s}", yellow("!")); - None - } + let filter = args.get(1).and_then(|s| match s.to_lowercase().as_str() { + "drafting" => Some(DraftStatus::Drafting), + "under_review" | "underreview" => Some(DraftStatus::UnderReview), + "approved" => Some(DraftStatus::Approved), + "published" => Some(DraftStatus::Published), + "rejected" => Some(DraftStatus::Rejected), + "superseded" => Some(DraftStatus::Superseded), + "archived" => Some(DraftStatus::Archived), + _ => { + eprintln!(" {} Invalid status: {s}", yellow("!")); + None } }); let drafts = state.engine.list_policy_drafts(filter)?; if state.json_mode { println!("{}", serde_json::to_string_pretty(&drafts)?); + } else if drafts.is_empty() { + println!(" {} No drafts found", yellow("!")); } else { - if drafts.is_empty() { - println!(" {} No drafts found", yellow("!")); - } else { - for d in &drafts { - println!(" {} {} [{}] {}", green("•"), d.id, d.status, d.name); - } + for d in &drafts { + println!(" {} {} [{}] {}", green("•"), d.id, d.status, d.name); } } } @@ -1234,24 +1429,37 @@ fn cmd_schedule(state: &mut ReplState, args: &[&str]) -> Result<()> { let json_str = std::fs::read_to_string(args[1]) .with_context(|| format!("failed to read config: {}", args[1]))?; let cfg: AnalysisScheduleConfig = serde_json::from_str(&json_str)?; - let schedule = state.engine.create_analysis_schedule(&cfg.name, cfg.interval_seconds, cfg.queries, cfg.compare_schema)?; + let schedule = state.engine.create_analysis_schedule( + &cfg.name, + cfg.interval_seconds, + cfg.queries, + cfg.compare_schema, + )?; if state.json_mode { println!("{}", serde_json::to_string_pretty(&schedule)?); } else { - println!(" {} Created schedule {} ({})", green("✓"), schedule.name, schedule.id); + println!( + " {} Created schedule {} ({})", + green("✓"), + schedule.name, + schedule.id + ); } } "list" => { let schedules = state.engine.list_analysis_schedules()?; if state.json_mode { println!("{}", serde_json::to_string_pretty(&schedules)?); + } else if schedules.is_empty() { + println!(" {} No schedules found", yellow("!")); } else { - if schedules.is_empty() { - println!(" {} No schedules found", yellow("!")); - } else { - for s in &schedules { - println!(" {} {} ({}s interval)", green("•"), s.name, s.interval_seconds); - } + for s in &schedules { + println!( + " {} {} ({}s interval)", + green("•"), + s.name, + s.interval_seconds + ); } } } @@ -1265,9 +1473,15 @@ fn cmd_schedule(state: &mut ReplState, args: &[&str]) -> Result<()> { if state.json_mode { println!(r#"{{"deleted":{deleted}}}"#); } else { - println!(" {} {}", + println!( + " {} {}", if deleted { green("✓") } else { yellow("!") }, - if deleted { "Schedule deleted" } else { "Schedule not found" }); + if deleted { + "Schedule deleted" + } else { + "Schedule not found" + } + ); } } "run" => { @@ -1284,14 +1498,20 @@ fn cmd_schedule(state: &mut ReplState, args: &[&str]) -> Result<()> { let runs = state.engine.get_analysis_runs(limit)?; if state.json_mode { println!("{}", serde_json::to_string_pretty(&runs)?); + } else if runs.is_empty() { + println!(" {} No runs found", yellow("!")); } else { - if runs.is_empty() { - println!(" {} No runs found", yellow("!")); - } else { - for r in &runs { - println!(" {} {} [{}]", green("•"), r.id, - if r.status == AnalysisRunStatus::Completed { "completed" } else { "failed" }); - } + for r in &runs { + println!( + " {} {} [{}]", + green("•"), + r.id, + if r.status == AnalysisRunStatus::Completed { + "completed" + } else { + "failed" + } + ); } } } @@ -1330,7 +1550,11 @@ fn cmd_enforcement(state: &mut ReplState, args: &[&str]) -> Result<()> { } else { println!(" {} Enabled: {}", bold("Enforcement"), config.enabled); println!(" {}: {:?}", bold("Sampling"), config.sampling); - println!(" {}: {}", bold("Max events/min"), config.max_events_per_minute); + println!( + " {}: {}", + bold("Max events/min"), + config.max_events_per_minute + ); println!(" {}: {}", bold("Max rows"), config.max_rows); println!(" {}: {} days", bold("Max age"), config.max_days); } @@ -1381,11 +1605,10 @@ fn cmd_subscribe(state: &mut ReplState, args: &[&str]) -> Result<()> { if state.json_mode { println!(r#"{{"status":"subscribed"}}"#); } else { - println!(" {} Subscribed to events. Type 'unwatch' to stop.", green("✓")); + println!( + " {} Subscribed to events. Type 'unwatch' to stop.", + green("✓") + ); } Ok(()) } - - - - diff --git a/crates/aegis-core/benches/cache.rs b/crates/aegis-core/benches/cache.rs index 2c390bf..38d66a7 100644 --- a/crates/aegis-core/benches/cache.rs +++ b/crates/aegis-core/benches/cache.rs @@ -1,9 +1,9 @@ -use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use criterion::{Criterion, black_box, criterion_group, criterion_main}; use aegis_core::engine::GraphEngine; -use aegis_core::types::schema::{PermissionDef, RelationDef, Schema, TypeDef}; -use aegis_core::storage::sqlite::{SqliteConfig, SqliteStorage}; use aegis_core::storage::StorageBackend; +use aegis_core::storage::sqlite::{SqliteConfig, SqliteStorage}; +use aegis_core::types::schema::{PermissionDef, RelationDef, Schema, TypeDef}; use aegis_core::types::*; use std::collections::HashMap; @@ -16,7 +16,10 @@ fn setup_engine_with_cache(capacity: usize) -> GraphEngine { let mut relations = HashMap::new(); relations.insert( "owner".to_string(), - RelationDef { inherit_from: vec![], description: None }, + RelationDef { + inherit_from: vec![], + description: None, + }, ); let mut permissions = HashMap::new(); permissions.insert( @@ -26,7 +29,14 @@ fn setup_engine_with_cache(capacity: usize) -> GraphEngine { ..Default::default() }, ); - types.insert("repo".to_string(), TypeDef { relations, permissions, ..Default::default() }); + types.insert( + "repo".to_string(), + TypeDef { + relations, + permissions, + ..Default::default() + }, + ); types }, }; @@ -35,10 +45,14 @@ fn setup_engine_with_cache(capacity: usize) -> GraphEngine { let engine = GraphEngine::new(Box::new(storage), schema).with_cache_capacity(capacity); for i in 0..500 { - let subject = SubjectId::new(&format!("user:{}", i)).unwrap(); - let repo = ResourceId::new(&format!("repo:bench{}", i)).unwrap(); + let subject = SubjectId::new(format!("user:{}", i)).unwrap(); + let repo = ResourceId::new(format!("repo:bench{}", i)).unwrap(); engine - .write(&RelationshipTuple::new(subject, Relation::new("owner").unwrap(), repo)) + .write(&RelationshipTuple::new( + subject, + Relation::new("owner").unwrap(), + repo, + )) .unwrap(); } engine @@ -51,9 +65,13 @@ fn bench_cache_lru_zipfian(c: &mut Criterion) { b.iter(|| { let i = fastrand::usize(0..500); let hot = i < 100; - let idx = if hot { fastrand::usize(0..100) } else { fastrand::usize(100..500) }; - let subject = SubjectId::new(&format!("user:{}", idx)).unwrap(); - let repo = ResourceId::new(&format!("repo:bench{}", idx)).unwrap(); + let idx = if hot { + fastrand::usize(0..100) + } else { + fastrand::usize(100..500) + }; + let subject = SubjectId::new(format!("user:{}", idx)).unwrap(); + let repo = ResourceId::new(format!("repo:bench{}", idx)).unwrap(); let result = engine.check(black_box(&subject), "read", black_box(&repo), None); black_box(result) }) diff --git a/crates/aegis-core/benches/check.rs b/crates/aegis-core/benches/check.rs index 7fe36c3..a0f0bb3 100644 --- a/crates/aegis-core/benches/check.rs +++ b/crates/aegis-core/benches/check.rs @@ -1,9 +1,9 @@ -use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use criterion::{Criterion, black_box, criterion_group, criterion_main}; use aegis_core::engine::GraphEngine; -use aegis_core::types::schema::{PermissionDef, RelationDef, Schema, TypeDef}; use aegis_core::storage::sqlite::{SqliteConfig, SqliteStorage}; use aegis_core::storage::{StorageBackend, TupleFilter}; +use aegis_core::types::schema::{PermissionDef, RelationDef, Schema, TypeDef}; use aegis_core::types::*; use std::collections::HashMap; @@ -16,11 +16,17 @@ fn setup_engine() -> GraphEngine { let mut relations = HashMap::new(); relations.insert( "owner".to_string(), - RelationDef { inherit_from: vec![], description: None }, + RelationDef { + inherit_from: vec![], + description: None, + }, ); relations.insert( "viewer".to_string(), - RelationDef { inherit_from: vec![], description: None }, + RelationDef { + inherit_from: vec![], + description: None, + }, ); let mut permissions = HashMap::new(); permissions.insert( @@ -30,7 +36,14 @@ fn setup_engine() -> GraphEngine { ..Default::default() }, ); - types.insert("repo".to_string(), TypeDef { relations, permissions, ..Default::default() }); + types.insert( + "repo".to_string(), + TypeDef { + relations, + permissions, + ..Default::default() + }, + ); types }, }; @@ -40,10 +53,14 @@ fn setup_engine() -> GraphEngine { // Seed tuples for i in 0..1000 { - let subject = SubjectId::new(&format!("user:{}", i)).unwrap(); - let repo = ResourceId::new(&format!("repo:bench{}", i)).unwrap(); + let subject = SubjectId::new(format!("user:{}", i)).unwrap(); + let repo = ResourceId::new(format!("repo:bench{}", i)).unwrap(); engine - .write(&RelationshipTuple::new(subject, Relation::new("owner").unwrap(), repo)) + .write(&RelationshipTuple::new( + subject, + Relation::new("owner").unwrap(), + repo, + )) .unwrap(); } engine diff --git a/crates/aegis-core/benches/partition.rs b/crates/aegis-core/benches/partition.rs index 3a8701e..668651f 100644 --- a/crates/aegis-core/benches/partition.rs +++ b/crates/aegis-core/benches/partition.rs @@ -1,9 +1,9 @@ -use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use criterion::{Criterion, black_box, criterion_group, criterion_main}; use aegis_core::engine::GraphEngine; -use aegis_core::types::schema::{PermissionDef, RelationDef, Schema, TypeDef}; -use aegis_core::storage::sqlite::{SqliteConfig, SqliteStorage}; use aegis_core::storage::StorageBackend; +use aegis_core::storage::sqlite::{SqliteConfig, SqliteStorage}; +use aegis_core::types::schema::{PermissionDef, RelationDef, Schema, TypeDef}; use aegis_core::types::*; use std::collections::HashMap; @@ -19,7 +19,10 @@ fn setup_partitions() -> GraphEngine { let mut relations = HashMap::new(); relations.insert( "owner".to_string(), - RelationDef { inherit_from: vec![], description: None }, + RelationDef { + inherit_from: vec![], + description: None, + }, ); let mut permissions = HashMap::new(); permissions.insert( @@ -29,7 +32,14 @@ fn setup_partitions() -> GraphEngine { ..Default::default() }, ); - types.insert("repo".to_string(), TypeDef { relations, permissions, ..Default::default() }); + types.insert( + "repo".to_string(), + TypeDef { + relations, + permissions, + ..Default::default() + }, + ); types }, }; @@ -38,12 +48,12 @@ fn setup_partitions() -> GraphEngine { let engine = GraphEngine::new(Box::new(storage), schema); for pi in 0..NUM_PARTITIONS { - let pid = PartitionId::new(&format!("p{:04}", pi)).unwrap(); + let pid = PartitionId::new(format!("p{:04}", pi)).unwrap(); engine.with_partition(pid.clone()).unwrap(); for ti in 0..TUPLES_PER_PARTITION { - let subject = SubjectId::new(&format!("user:{}", ti)).unwrap(); - let repo = ResourceId::new(&format!("repo:bench{}", ti)).unwrap(); + let subject = SubjectId::new(format!("user:{}", ti)).unwrap(); + let repo = ResourceId::new(format!("repo:bench{}", ti)).unwrap(); engine .write(&RelationshipTuple::new( subject, @@ -85,12 +95,15 @@ fn bench_partition_check_throughput(c: &mut Criterion) { c.bench_function("partition_check", |b| { b.iter(|| { - let result = - engine.check(black_box(&subject), "read", black_box(&resource), None); + let result = engine.check(black_box(&subject), "read", black_box(&resource), None); black_box(result) }) }); } -criterion_group!(benches, bench_partition_write_throughput, bench_partition_check_throughput); +criterion_group!( + benches, + bench_partition_write_throughput, + bench_partition_check_throughput +); criterion_main!(benches); diff --git a/crates/aegis-core/src/engine/acl.rs b/crates/aegis-core/src/engine/acl.rs index b4c6626..cbed4b0 100644 --- a/crates/aegis-core/src/engine/acl.rs +++ b/crates/aegis-core/src/engine/acl.rs @@ -14,24 +14,23 @@ pub fn grant( ) -> AegisResult { let resource_type = resource_type_name(resource.as_str()); let schema = engine.schema(); - let rels = schema.relations_for_permission(&resource_type, permission) - .ok_or_else(|| AegisError::SchemaValidation( - format!("permission '{permission}' not found for type '{resource_type}'") - ))? + let rels = schema + .relations_for_permission(&resource_type, permission) + .ok_or_else(|| { + AegisError::SchemaValidation(format!( + "permission '{permission}' not found for type '{resource_type}'" + )) + })? .clone(); drop(schema); if rels.is_empty() { - return Err(AegisError::SchemaValidation( - format!("permission '{permission}' has no granting relations") - )); + return Err(AegisError::SchemaValidation(format!( + "permission '{permission}' has no granting relations" + ))); } - let tuple = RelationshipTuple::new( - subject.clone(), - Relation::new(&rels[0])?, - resource.clone(), - ); + let tuple = RelationshipTuple::new(subject.clone(), Relation::new(&rels[0])?, resource.clone()); engine.write(&tuple) } @@ -44,17 +43,20 @@ pub fn revoke( ) -> AegisResult { let resource_type = resource_type_name(resource.as_str()); let schema = engine.schema(); - let rels = schema.relations_for_permission(&resource_type, permission) - .ok_or_else(|| AegisError::SchemaValidation( - format!("permission '{permission}' not found for type '{resource_type}'") - ))? + let rels = schema + .relations_for_permission(&resource_type, permission) + .ok_or_else(|| { + AegisError::SchemaValidation(format!( + "permission '{permission}' not found for type '{resource_type}'" + )) + })? .clone(); drop(schema); if rels.is_empty() { - return Err(AegisError::SchemaValidation( - format!("permission '{permission}' has no granting relations") - )); + return Err(AegisError::SchemaValidation(format!( + "permission '{permission}' has no granting relations" + ))); } let key = TupleKey { @@ -96,19 +98,19 @@ pub struct SerializedAclCollection { } /// Export all ACL entries for a given resource as JSON. -pub fn serialize_acls( - engine: &GraphEngine, - resource: &ResourceId, -) -> AegisResult { +pub fn serialize_acls(engine: &GraphEngine, resource: &ResourceId) -> AegisResult { let tuples = engine.list_by_object(resource, None, None)?; let schema = engine.schema(); - let entries: Vec = tuples.iter().map(|t| SerializedAclEntry { - subject: t.subject.to_string(), - relation: t.relation.to_string(), - object: t.object.to_string(), - metadata: t.metadata.clone(), - condition: t.condition.clone(), - }).collect(); + let entries: Vec = tuples + .iter() + .map(|t| SerializedAclEntry { + subject: t.subject.to_string(), + relation: t.relation.to_string(), + object: t.object.to_string(), + metadata: t.metadata.clone(), + condition: t.condition.clone(), + }) + .collect(); let collection = SerializedAclCollection { schema_version: schema.schema_version, namespace: schema.namespace.clone(), @@ -121,30 +123,26 @@ pub fn serialize_acls( /// Import ACL entries from JSON, writing each entry as a relationship tuple. /// Returns a list of revision tokens, one per successful write. -pub fn deserialize_acls( - engine: &GraphEngine, - json: &str, -) -> AegisResult> { - let collection: SerializedAclCollection = serde_json::from_str(json) - .map_err(|e| AegisError::MetadataValidation(e.to_string()))?; +pub fn deserialize_acls(engine: &GraphEngine, json: &str) -> AegisResult> { + let collection: SerializedAclCollection = + serde_json::from_str(json).map_err(|e| AegisError::MetadataValidation(e.to_string()))?; let mut tokens = Vec::new(); for entry in &collection.entries { - let subject = SubjectId::new(&entry.subject) - .map_err(|e| AegisError::Validation(e))?; - let relation = Relation::new(&entry.relation) - .map_err(|e| AegisError::Validation(e))?; - let object = ResourceId::new(&entry.object) - .map_err(|e| AegisError::Validation(e))?; + let subject = SubjectId::new(&entry.subject).map_err(AegisError::Validation)?; + let relation = Relation::new(&entry.relation).map_err(AegisError::Validation)?; + let object = ResourceId::new(&entry.object).map_err(AegisError::Validation)?; let tuple = match (&entry.metadata, &entry.condition) { (Some(meta), Some(cond)) => { - let mut t = RelationshipTuple::with_condition(subject, relation, object, cond.clone()); + let mut t = + RelationshipTuple::with_condition(subject, relation, object, cond.clone()); t.metadata = Some(meta.clone()); t } - (Some(meta), None) => RelationshipTuple::with_metadata( - subject, relation, object, meta.clone(), - ).map_err(|e| AegisError::MetadataValidation(e.to_string()))?, + (Some(meta), None) => { + RelationshipTuple::with_metadata(subject, relation, object, meta.clone()) + .map_err(|e| AegisError::MetadataValidation(e.to_string()))? + } (None, Some(cond)) => { RelationshipTuple::with_condition(subject, relation, object, cond.clone()) } @@ -158,9 +156,9 @@ pub fn deserialize_acls( #[cfg(all(test, feature = "sqlite"))] mod tests { use super::*; + use crate::storage::StorageBackend; #[cfg(feature = "sqlite")] use crate::storage::sqlite::{SqliteConfig, SqliteStorage}; - use crate::storage::StorageBackend; use crate::types::schema::*; fn make_engine() -> GraphEngine { @@ -170,16 +168,38 @@ mod tests { types: { let mut types = std::collections::HashMap::new(); let mut relations = std::collections::HashMap::new(); - relations.insert("owner".to_string(), RelationDef { inherit_from: vec![], description: None }); - relations.insert("viewer".to_string(), RelationDef { inherit_from: vec![], description: None }); + relations.insert( + "owner".to_string(), + RelationDef { + inherit_from: vec![], + description: None, + }, + ); + relations.insert( + "viewer".to_string(), + RelationDef { + inherit_from: vec![], + description: None, + }, + ); let mut permissions = std::collections::HashMap::new(); - permissions.insert("read".to_string(), PermissionDef { - union_of: vec!["viewer".to_string(), "owner".to_string()], - condition: None, - description: None, - ..Default::default() - }); - types.insert("repo".to_string(), TypeDef { relations, permissions, ..Default::default() }); + permissions.insert( + "read".to_string(), + PermissionDef { + union_of: vec!["viewer".to_string(), "owner".to_string()], + condition: None, + description: None, + ..Default::default() + }, + ); + types.insert( + "repo".to_string(), + TypeDef { + relations, + permissions, + ..Default::default() + }, + ); types }, }; @@ -193,7 +213,13 @@ mod tests { let engine = make_engine(); let alice = SubjectId::new("user:alice").unwrap(); let repo = ResourceId::new("repo:fluxbus").unwrap(); - engine.write(&RelationshipTuple::new(alice.clone(), Relation::new("owner").unwrap(), repo.clone())).unwrap(); + engine + .write(&RelationshipTuple::new( + alice.clone(), + Relation::new("owner").unwrap(), + repo.clone(), + )) + .unwrap(); let json = serialize_acls(&engine, &repo).unwrap(); assert!(json.contains("user:alice")); @@ -208,10 +234,14 @@ mod tests { let engine = make_engine(); let alice = SubjectId::new("user:alice").unwrap(); let repo = ResourceId::new("repo:fluxbus").unwrap(); - engine.write(&RelationshipTuple::with_condition( - alice.clone(), Relation::new("viewer").unwrap(), repo.clone(), - "role eq admin".to_string(), - )).unwrap(); + engine + .write(&RelationshipTuple::with_condition( + alice.clone(), + Relation::new("viewer").unwrap(), + repo.clone(), + "role eq admin".to_string(), + )) + .unwrap(); let json = serialize_acls(&engine, &repo).unwrap(); assert!(json.contains("role eq admin")); @@ -219,7 +249,11 @@ mod tests { let _tokens = deserialize_acls(&engine, &json).unwrap(); // Verify written tuple has condition let tuples = engine.list_by_object(&repo, None, None).unwrap(); - assert!(tuples.iter().any(|t| t.condition.as_deref() == Some("role eq admin"))); + assert!( + tuples + .iter() + .any(|t| t.condition.as_deref() == Some("role eq admin")) + ); } #[test] diff --git a/crates/aegis-core/src/engine/analysis/graph.rs b/crates/aegis-core/src/engine/analysis/graph.rs index 7b05255..ac7d384 100644 --- a/crates/aegis-core/src/engine/analysis/graph.rs +++ b/crates/aegis-core/src/engine/analysis/graph.rs @@ -1,10 +1,10 @@ -use std::collections::{HashMap, HashSet, VecDeque}; -use std::time::Instant; use crate::engine::GraphEngine; use crate::error::AegisResult; use crate::storage::{StorageBackend, TupleFilter}; use crate::types::analysis::*; use crate::types::*; +use std::collections::{HashMap, HashSet, VecDeque}; +use std::time::Instant; impl GraphEngine { /// Find all subjects reachable from a resource through the authorization graph. @@ -20,6 +20,7 @@ impl GraphEngine { ) -> AegisResult { // Cache check let cache_key = format!("reach:{}:{}:{}", resource.as_str(), max_depth, max_nodes); + #[allow(clippy::collapsible_if)] if let Some(ttl) = cache_ttl_ms { if let Some(cached) = self.get_cached_analysis(&cache_key, ttl) { return Ok(cached); @@ -112,7 +113,10 @@ impl GraphEngine { .query_tuples( &pid, &TupleFilter::default(), - &PaginationParams { cursor: None, limit: 1_000_000 }, + &PaginationParams { + cursor: None, + limit: 1_000_000, + }, &ConsistencyMode::MinimizeLatency, ) .map_err(|e| crate::error::AegisError::Internal(e.to_string()))?; @@ -122,10 +126,14 @@ impl GraphEngine { for t in &all.tuples { let resource_type = t.object.as_str().split(':').next().unwrap_or(""); let type_def = schema.types.get(resource_type); - let relation_valid = type_def.map_or(false, |td| { + let relation_valid = type_def.is_some_and(|td| { td.relations.contains_key(t.relation.as_str()) || td.permissions.contains_key(t.relation.as_str()) - || td.deny.iter().any(|d| d.relations.iter().any(|r| r.as_str() == t.relation.as_str())) + || td.deny.iter().any(|d| { + d.relations + .iter() + .any(|r| r.as_str() == t.relation.as_str()) + }) }); if !relation_valid { @@ -150,7 +158,10 @@ impl GraphEngine { .query_tuples( &pid, &TupleFilter::default(), - &PaginationParams { cursor: None, limit: 1_000_000 }, + &PaginationParams { + cursor: None, + limit: 1_000_000, + }, &ConsistencyMode::MinimizeLatency, ) .map_err(|e| crate::error::AegisError::Internal(e.to_string()))?; @@ -178,7 +189,7 @@ impl GraphEngine { }) .collect(); - result.sort_by(|a, b| b.resource_count.cmp(&a.resource_count)); + result.sort_by_key(|b| std::cmp::Reverse(b.resource_count)); Ok(result) } @@ -200,6 +211,7 @@ impl GraphEngine { } fn set_cached_analysis(&self, key: &str, value: &impl serde::Serialize, ttl_ms: u64) { + #[allow(clippy::collapsible_if)] if let Ok(mut cache) = self.analysis_cache.lock() { if let Ok(json) = serde_json::to_string(value) { cache.insert(key.to_string(), (Instant::now(), ttl_ms, json)); @@ -217,7 +229,10 @@ pub fn detect_tenant_leakage( .query_tuples( &default_pid, &TupleFilter::default(), - &PaginationParams { cursor: None, limit: 1_000_000 }, + &PaginationParams { + cursor: None, + limit: 1_000_000, + }, &ConsistencyMode::MinimizeLatency, ) .map_err(|e| crate::error::AegisError::Internal(e.to_string()))?; diff --git a/crates/aegis-core/src/engine/analysis/mod.rs b/crates/aegis-core/src/engine/analysis/mod.rs index cc56d0e..81b0d8c 100644 --- a/crates/aegis-core/src/engine/analysis/mod.rs +++ b/crates/aegis-core/src/engine/analysis/mod.rs @@ -39,17 +39,16 @@ impl GraphEngine { }; drop(schema); - let all_traces; - let allowed; let mut denial_reason = None::; let traversal_result = self.explain(subject, permission, resource, consistency)?; - allowed = traversal_result.allowed; - all_traces = traversal_result.trace; + let allowed = traversal_result.allowed; + let all_traces = traversal_result.trace; if !allowed { let schema = self.schema.read().unwrap(); let type_def = schema.types.get(&resource_type); + #[allow(clippy::collapsible_if)] if let Some(type_def) = type_def { if !type_def.deny.is_empty() { 'deny_check: for deny_def in &type_def.deny { @@ -58,6 +57,7 @@ impl GraphEngine { Ok(r) => r, Err(_) => continue, }; + #[allow(clippy::collapsible_if)] if let Ok(tr) = crate::engine::traversal::bfs_traversal( &self.active_partition_id(), self.storage.as_ref(), @@ -154,7 +154,12 @@ impl GraphEngine { Ok(r) => r, Err(_) => continue, }; - let tuples = match storage.list_by_object(&pid, resource, Some(&relation), &ConsistencyMode::MinimizeLatency) { + let tuples = match storage.list_by_object( + &pid, + resource, + Some(&relation), + &ConsistencyMode::MinimizeLatency, + ) { Ok(t) => t, Err(_) => continue, }; @@ -203,14 +208,18 @@ impl GraphEngine { Err(_) => continue, }; for t in &ok { - let new_subj = format!("{}#{}", t.subject.as_str(), t.relation.as_str()); + let new_subj = + format!("{}#{}", t.subject.as_str(), t.relation.as_str()); let mut new_paths = paths.clone(); new_paths.push(ExplainTrace { subject: new_subj.clone(), relation: t.relation.as_str().to_string(), object: t.object.as_str().to_string(), }); - new_candidates.entry(new_subj).or_default().extend(new_paths); + new_candidates + .entry(new_subj) + .or_default() + .extend(new_paths); } } } @@ -227,12 +236,17 @@ impl GraphEngine { let total = candidates.len() as u64; // Apply pagination - let offset = pagination.cursor.as_ref().map(|c| c.offset as usize).unwrap_or(0); + let offset = pagination + .cursor + .as_ref() + .map(|c| c.offset as usize) + .unwrap_or(0); let limit = pagination.limit as usize; let mut all_subjects: Vec<(String, Vec)> = candidates.into_iter().collect(); all_subjects.sort_by(|a, b| a.0.cmp(&b.0)); let has_more = offset + limit < total as usize; - let page: Vec<(String, Vec)> = all_subjects.into_iter().skip(offset).take(limit).collect(); + let page: Vec<(String, Vec)> = + all_subjects.into_iter().skip(offset).take(limit).collect(); let subjects: Vec = page .into_iter() @@ -267,12 +281,18 @@ impl GraphEngine { let _revision = self.resolve_revision(None)?; // Collect all tuples - let all_tuples = self.storage.query_tuples( - &pid, - &TupleFilter::default(), - &PaginationParams { cursor: None, limit: 1_000_000 }, - &ConsistencyMode::MinimizeLatency, - ).map_err(|e| AegisError::Internal(e.to_string()))?; + let all_tuples = self + .storage + .query_tuples( + &pid, + &TupleFilter::default(), + &PaginationParams { + cursor: None, + limit: 1_000_000, + }, + &ConsistencyMode::MinimizeLatency, + ) + .map_err(|e| AegisError::Internal(e.to_string()))?; let mut gained = Vec::new(); let mut lost = Vec::new(); @@ -284,7 +304,9 @@ impl GraphEngine { break; } - let subject_filter = subject_sample.map(|s| s.contains(&t.subject)).unwrap_or(true); + let subject_filter = subject_sample + .map(|s| s.contains(&t.subject)) + .unwrap_or(true); if !subject_filter { continue; } @@ -309,18 +331,20 @@ impl GraphEngine { break; } - let resolved_before = crate::engine::policy::resolve_permission(schema_before, &resource_type, perm); - let resolved_after = crate::engine::policy::resolve_permission(schema_after, &resource_type, perm); + let resolved_before = + crate::engine::policy::resolve_permission(schema_before, &resource_type, perm); + let resolved_after = + crate::engine::policy::resolve_permission(schema_after, &resource_type, perm); - let before_allowed = resolved_before.map_or(false, |r| { - r.relations.iter().any(|rel| { - rel.as_str() == t.relation.as_str() - }) + let before_allowed = resolved_before.is_some_and(|r| { + r.relations + .iter() + .any(|rel| rel.as_str() == t.relation.as_str()) }); - let after_allowed = resolved_after.map_or(false, |r| { - r.relations.iter().any(|rel| { - rel.as_str() == t.relation.as_str() - }) + let after_allowed = resolved_after.is_some_and(|r| { + r.relations + .iter() + .any(|rel| rel.as_str() == t.relation.as_str()) }); let subj_str = t.subject.as_str().to_string(); @@ -354,7 +378,11 @@ impl GraphEngine { } /// Build an analysis report for export. - pub fn analysis_report(&self, report_type: &str, data: serde_json::Value) -> AegisResult { + pub fn analysis_report( + &self, + report_type: &str, + data: serde_json::Value, + ) -> AegisResult { Ok(AnalysisReport { report_type: report_type.to_string(), generated_at: chrono::Utc::now().to_rfc3339(), diff --git a/crates/aegis-core/src/engine/analysis/simulate.rs b/crates/aegis-core/src/engine/analysis/simulate.rs index 2964667..8c12ab2 100644 --- a/crates/aegis-core/src/engine/analysis/simulate.rs +++ b/crates/aegis-core/src/engine/analysis/simulate.rs @@ -24,7 +24,9 @@ impl GraphEngine { let key = format!("{}:{}:{}", q.subject, q.permission, q.resource); let subject = SubjectId::new(&q.subject)?; let resource = ResourceId::new(&q.resource)?; - let allowed = self.check(&subject, &q.permission, &resource, None)?.allowed; + let allowed = self + .check(&subject, &q.permission, &resource, None)? + .allowed; before_results.insert(key, allowed); } @@ -32,10 +34,10 @@ impl GraphEngine { let overlay = InMemoryOverlay::new(self.storage.as_ref()); for t in add { - let _ = overlay.write_tuple_internal(&pid, t); + overlay.write_tuple_internal(&pid, t); } for k in remove { - let _ = overlay.delete_tuple_internal(&pid, k); + overlay.delete_tuple_internal(&pid, k); } // Evaluate checks against modified state @@ -59,9 +61,17 @@ impl GraphEngine { }; match (before, after) { - (true, false) => { lost += 1; flips.push(flip); } - (false, true) => { gained += 1; flips.push(flip); } - _ => { unchanged += 1; } + (true, false) => { + lost += 1; + flips.push(flip); + } + (false, true) => { + gained += 1; + flips.push(flip); + } + _ => { + unchanged += 1; + } } } diff --git a/crates/aegis-core/src/engine/cache.rs b/crates/aegis-core/src/engine/cache.rs index 7deea4c..bf3a73f 100644 --- a/crates/aegis-core/src/engine/cache.rs +++ b/crates/aegis-core/src/engine/cache.rs @@ -63,7 +63,7 @@ impl DecisionCache { partition_id.to_string(), ); - let is_valid = self.entries.get(&key).map_or(false, |entry| { + let is_valid = self.entries.get(&key).is_some_and(|entry| { entry.revision >= current_revision && entry.created_at.elapsed() < self.ttl }); @@ -108,6 +108,7 @@ impl DecisionCache { self.access_order.retain(|k| k != &key); // Evict LRU entry if at capacity + #[allow(clippy::collapsible_if)] if self.entries.len() >= self.capacity { if let Some(lru_key) = self.access_order.pop_front() { self.entries.remove(&lru_key); @@ -192,14 +193,19 @@ impl TraversalCache { current_revision: Revision, ) -> Option> { let key = (subject.to_string(), relation.to_string()); - let is_valid = self.entries.get(&key).map_or(false, |(_, rev)| *rev >= current_revision); + let is_valid = self + .entries + .get(&key) + .is_some_and(|(_, rev)| *rev >= current_revision); if is_valid { // Move to MRU position if let Some(pos) = self.access_order.iter().position(|k| k == &key) { self.access_order.remove(pos); self.access_order.push_back(key.clone()); } - self.entries.get(&key).map(|(resources, _)| resources.clone()) + self.entries + .get(&key) + .map(|(resources, _)| resources.clone()) } else { if self.entries.contains_key(&key) { self.entries.remove(&key); @@ -223,6 +229,7 @@ impl TraversalCache { self.access_order.retain(|k| k != &key); // Evict LRU entry if at capacity + #[allow(clippy::collapsible_if)] if self.entries.len() >= self.capacity { if let Some(lru_key) = self.access_order.pop_front() { self.entries.remove(&lru_key); @@ -251,7 +258,14 @@ mod tests { #[test] fn test_cache_hit() { let mut cache = DecisionCache::new(100); - cache.insert("user:1", "read", "repo:a", "default", true, Revision::new(5)); + cache.insert( + "user:1", + "read", + "repo:a", + "default", + true, + Revision::new(5), + ); assert_eq!( cache.get("user:1", "read", "repo:a", "default", Revision::new(5)), @@ -272,7 +286,14 @@ mod tests { #[test] fn test_cache_stale_eviction() { let mut cache = DecisionCache::new(100); - cache.insert("user:1", "read", "repo:a", "default", true, Revision::new(5)); + cache.insert( + "user:1", + "read", + "repo:a", + "default", + true, + Revision::new(5), + ); // Revision 10 > 5 → entry is stale assert_eq!( @@ -284,9 +305,30 @@ mod tests { #[test] fn test_cache_capacity() { let mut cache = DecisionCache::new(2); - cache.insert("user:1", "read", "repo:a", "default", true, Revision::new(1)); - cache.insert("user:2", "read", "repo:b", "default", true, Revision::new(2)); - cache.insert("user:3", "read", "repo:c", "default", true, Revision::new(3)); + cache.insert( + "user:1", + "read", + "repo:a", + "default", + true, + Revision::new(1), + ); + cache.insert( + "user:2", + "read", + "repo:b", + "default", + true, + Revision::new(2), + ); + cache.insert( + "user:3", + "read", + "repo:c", + "default", + true, + Revision::new(3), + ); // At most 2 entries should remain (one evicted due to capacity) assert!(cache.len() <= 2); @@ -295,7 +337,14 @@ mod tests { #[test] fn test_cache_clear() { let mut cache = DecisionCache::new(100); - cache.insert("user:1", "read", "repo:a", "default", true, Revision::new(1)); + cache.insert( + "user:1", + "read", + "repo:a", + "default", + true, + Revision::new(1), + ); cache.clear(); assert!(cache.is_empty()); assert_eq!(cache.hit_rate(), 0.0); @@ -314,7 +363,12 @@ mod tests { #[test] fn test_traversal_cache_stale() { let mut cache = TraversalCache::new(100); - cache.insert("user:1", "owner", vec!["repo:a".to_string()], Revision::new(5)); + cache.insert( + "user:1", + "owner", + vec!["repo:a".to_string()], + Revision::new(5), + ); assert_eq!(cache.get("user:1", "owner", Revision::new(10)), None); } @@ -324,23 +378,69 @@ mod tests { let mut cache = DecisionCache::new(3); // Fill cache to capacity - cache.insert("user:1", "read", "repo:a", "default", true, Revision::new(1)); - cache.insert("user:2", "read", "repo:b", "default", true, Revision::new(2)); - cache.insert("user:3", "read", "repo:c", "default", true, Revision::new(3)); + cache.insert( + "user:1", + "read", + "repo:a", + "default", + true, + Revision::new(1), + ); + cache.insert( + "user:2", + "read", + "repo:b", + "default", + true, + Revision::new(2), + ); + cache.insert( + "user:3", + "read", + "repo:c", + "default", + true, + Revision::new(3), + ); // Access user:1 and user:2 to make them MRU - assert_eq!(cache.get("user:1", "read", "repo:a", "default", Revision::new(1)), Some(true)); - assert_eq!(cache.get("user:2", "read", "repo:b", "default", Revision::new(2)), Some(true)); + assert_eq!( + cache.get("user:1", "read", "repo:a", "default", Revision::new(1)), + Some(true) + ); + assert_eq!( + cache.get("user:2", "read", "repo:b", "default", Revision::new(2)), + Some(true) + ); // Insert 4th entry — should evict LRU entry (user:3) - cache.insert("user:4", "read", "repo:d", "default", true, Revision::new(4)); + cache.insert( + "user:4", + "read", + "repo:d", + "default", + true, + Revision::new(4), + ); // user:3 should be evicted - assert_eq!(cache.get("user:3", "read", "repo:c", "default", Revision::new(3)), None); + assert_eq!( + cache.get("user:3", "read", "repo:c", "default", Revision::new(3)), + None + ); // user:1 and user:2 should still be present - assert_eq!(cache.get("user:1", "read", "repo:a", "default", Revision::new(1)), Some(true)); - assert_eq!(cache.get("user:2", "read", "repo:b", "default", Revision::new(2)), Some(true)); + assert_eq!( + cache.get("user:1", "read", "repo:a", "default", Revision::new(1)), + Some(true) + ); + assert_eq!( + cache.get("user:2", "read", "repo:b", "default", Revision::new(2)), + Some(true) + ); // user:4 should be present - assert_eq!(cache.get("user:4", "read", "repo:d", "default", Revision::new(4)), Some(true)); + assert_eq!( + cache.get("user:4", "read", "repo:d", "default", Revision::new(4)), + Some(true) + ); } } diff --git a/crates/aegis-core/src/engine/condition.rs b/crates/aegis-core/src/engine/condition.rs index c1376d3..e3eaadb 100644 --- a/crates/aegis-core/src/engine/condition.rs +++ b/crates/aegis-core/src/engine/condition.rs @@ -25,10 +25,7 @@ pub enum ConditionOp { #[derive(Debug, Clone)] pub enum ConditionExpr { - Leaf { - attr: String, - op: ConditionOp, - }, + Leaf { attr: String, op: ConditionOp }, And(Vec), Or(Vec), Not(Box), @@ -44,9 +41,10 @@ pub fn parse_condition(expr: &str) -> AegisResult { let inner_expr = parse_condition(&inner[1..inner.len() - 1])?; return Ok(ConditionExpr::Not(Box::new(inner_expr))); } - return Err(crate::error::AegisError::SchemaValidation( - format!("NOT condition must be parenthesized: {:?}", expr), - )); + return Err(crate::error::AegisError::SchemaValidation(format!( + "NOT condition must be parenthesized: {:?}", + expr + ))); } // Composite: (expr1) AND (expr2) or (expr1) OR (expr2) @@ -85,7 +83,10 @@ pub fn parse_condition(expr: &str) -> AegisResult { let left_str = trimmed[1..close].trim(); let offset = if op_type == Some("OR") { 4 } else { 5 }; let right_str = trimmed[pos + offset..].trim(); - let right_str = right_str.strip_prefix('(').and_then(|s| s.strip_suffix(')')).unwrap_or(right_str); + let right_str = right_str + .strip_prefix('(') + .and_then(|s| s.strip_suffix(')')) + .unwrap_or(right_str); let left = parse_condition(left_str)?; let right = parse_condition(right_str)?; return match op_type { @@ -99,9 +100,10 @@ pub fn parse_condition(expr: &str) -> AegisResult { // Leaf condition: "attr op value" let parts: Vec<&str> = trimmed.splitn(2, char::is_whitespace).collect(); if parts.len() < 2 { - return Err(crate::error::AegisError::SchemaValidation( - format!("invalid condition expression: {:?}", expr), - )); + return Err(crate::error::AegisError::SchemaValidation(format!( + "invalid condition expression: {:?}", + expr + ))); } let attr = parts[0].to_string(); let rest = parts[1].trim().to_string(); @@ -169,9 +171,10 @@ pub fn parse_condition(expr: &str) -> AegisResult { op: ConditionOp::DayOfWeek(items), }) } else { - Err(crate::error::AegisError::SchemaValidation( - format!("unknown condition operator in: {:?}", expr), - )) + Err(crate::error::AegisError::SchemaValidation(format!( + "unknown condition operator in: {:?}", + expr + ))) } } @@ -183,28 +186,28 @@ fn evaluate_leaf(attr: &str, op: &ConditionOp, ctx: &ConditionEvalContext) -> bo .or_else(|| ctx.env.get(attr)); match op { - ConditionOp::Eq(expected) => value.map_or(false, |v| v == expected), - ConditionOp::Neq(expected) => value.map_or(true, |v| v != expected), - ConditionOp::In(items) => value.map_or(false, |v| items.contains(v)), + ConditionOp::Eq(expected) => value == Some(expected), + ConditionOp::Neq(expected) => value != Some(expected), + ConditionOp::In(items) => value.is_some_and(|v| items.contains(v)), ConditionOp::Exists => value.is_some(), ConditionOp::NotExists => value.is_none(), ConditionOp::Gt(expected) => value .and_then(|v| v.parse::().ok()) .zip(expected.parse::().ok()) - .map_or(false, |(v, e)| v > e), + .is_some_and(|(v, e)| v > e), ConditionOp::Lt(expected) => value .and_then(|v| v.parse::().ok()) .zip(expected.parse::().ok()) - .map_or(false, |(v, e)| v < e), + .is_some_and(|(v, e)| v < e), ConditionOp::Before(time_str) => { let now = Utc::now(); let parsed = parse_time(time_str); - parsed.map_or(false, |t| now < t) + parsed.is_some_and(|t| now < t) } ConditionOp::After(time_str) => { let now = Utc::now(); let parsed = parse_time(time_str); - parsed.map_or(false, |t| now > t) + parsed.is_some_and(|t| now > t) } ConditionOp::DayOfWeek(days) => { let now = Utc::now(); diff --git a/crates/aegis-core/src/engine/enforcement_history.rs b/crates/aegis-core/src/engine/enforcement_history.rs index f81755c..294ebf7 100644 --- a/crates/aegis-core/src/engine/enforcement_history.rs +++ b/crates/aegis-core/src/engine/enforcement_history.rs @@ -107,13 +107,22 @@ impl RateTracker { impl GraphEngine { /// Configure enforcement history recording. - pub fn set_enforcement_history_config(&self, config: EnforcementHistoryConfig) -> AegisResult<()> { + pub fn set_enforcement_history_config( + &self, + config: EnforcementHistoryConfig, + ) -> AegisResult<()> { { - let mut cfg = self.enforcement_config.lock().map_err(|e| AegisError::Internal(e.to_string()))?; + let mut cfg = self + .enforcement_config + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; *cfg = config.clone(); } { - let mut rt = self.enforcement_rate_tracker.lock().map_err(|e| AegisError::Internal(e.to_string()))?; + let mut rt = self + .enforcement_rate_tracker + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; rt.update_max(config.max_events_per_minute); } Ok(()) @@ -121,14 +130,20 @@ impl GraphEngine { /// Get the current enforcement history configuration. pub fn get_enforcement_history_config(&self) -> AegisResult { - let cfg = self.enforcement_config.lock().map_err(|e| AegisError::Internal(e.to_string()))?; + let cfg = self + .enforcement_config + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; Ok(cfg.clone()) } /// Query enforcement trends. pub fn enforcement_trends(&self, limit: usize) -> AegisResult { let mut events = { - let e = self.enforcement_events.lock().map_err(|e| AegisError::Internal(e.to_string()))?; + let e = self + .enforcement_events + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; e.iter().rev().take(limit).cloned().collect::>() }; events.reverse(); @@ -137,12 +152,13 @@ impl GraphEngine { let denied_count = events.iter().filter(|e| !e.allowed).count() as u64; let allowed_count = events.iter().filter(|e| e.allowed).count() as u64; - let mut resource_counts: std::collections::HashMap = std::collections::HashMap::new(); + let mut resource_counts: std::collections::HashMap = + std::collections::HashMap::new(); for e in &events { *resource_counts.entry(e.resource.clone()).or_default() += 1; } let mut by_resource: Vec<(String, u64)> = resource_counts.into_iter().collect(); - by_resource.sort_by(|a, b| b.1.cmp(&a.1)); + by_resource.sort_by_key(|b| std::cmp::Reverse(b.1)); Ok(EnforcementTrends { total_events, @@ -224,6 +240,8 @@ impl GraphEngine { } // Periodically purge expired events (every ~1000 records) + // Periodically purge expired events (every ~1000 records) + #[allow(clippy::collapsible_if)] if cfg.max_days > 0 { if let Ok(mut events) = self.enforcement_events.lock() { if events.len() % 1000 == 0 { @@ -242,13 +260,13 @@ impl GraphEngine { } } -#[cfg(test)] +#[cfg(all(test, feature = "sqlite"))] mod tests { use super::*; use crate::engine::GraphEngine; + use crate::storage::StorageBackend; #[cfg(feature = "sqlite")] use crate::storage::sqlite::{SqliteConfig, SqliteStorage}; - use crate::storage::StorageBackend; use crate::types::*; use std::sync::Arc; diff --git a/crates/aegis-core/src/engine/gdpr.rs b/crates/aegis-core/src/engine/gdpr.rs index 019681e..78241e2 100644 --- a/crates/aegis-core/src/engine/gdpr.rs +++ b/crates/aegis-core/src/engine/gdpr.rs @@ -6,7 +6,8 @@ use crate::engine::GraphEngine; use crate::error::{AegisError, AegisResult}; use crate::types::{ - AuditEntry, ConsistencyMode, PaginationParams, PartitionId, RelationshipTuple, Revision, SubjectId, + AuditEntry, ConsistencyMode, PaginationParams, PartitionId, RelationshipTuple, Revision, + SubjectId, }; use chrono::{DateTime, Days, Utc}; use serde::{Deserialize, Serialize}; @@ -76,15 +77,23 @@ impl SignedExport { // Verify Ed25519 signature let pub_key = ed25519_dalek::VerifyingKey::from_bytes( - public_key.try_into().map_err(|_| "invalid public key length".to_string())? - ).map_err(|e| format!("invalid public key: {e}"))?; - - Ok(pub_key.verify_strict( - &computed_hash.as_bytes(), - &ed25519_dalek::Signature::from_bytes( - self.signature.as_slice().try_into().map_err(|_| "invalid signature length".to_string())? - ), - ).is_ok()) + public_key + .try_into() + .map_err(|_| "invalid public key length".to_string())?, + ) + .map_err(|e| format!("invalid public key: {e}"))?; + + Ok(pub_key + .verify_strict( + computed_hash.as_bytes(), + &ed25519_dalek::Signature::from_bytes( + self.signature + .as_slice() + .try_into() + .map_err(|_| "invalid signature length".to_string())?, + ), + ) + .is_ok()) } } @@ -106,8 +115,17 @@ impl<'a> GdprManager<'a> { } } - pub fn new_with_config(engine: &'a GraphEngine, partition_id: PartitionId, config: GdprConfig) -> Self { - Self { engine, partition_id, config, signing_key: None } + pub fn new_with_config( + engine: &'a GraphEngine, + partition_id: PartitionId, + config: GdprConfig, + ) -> Self { + Self { + engine, + partition_id, + config, + signing_key: None, + } } pub fn config(&self) -> &GdprConfig { @@ -168,20 +186,28 @@ impl<'a> GdprManager<'a> { /// Returns active tuples and audit entries for the given subject. pub fn export_subject_data(&self, subject: &SubjectId) -> AegisResult { let revision = self.engine.storage().current_revision(&self.partition_id)?; - let active_tuples = self.engine.storage().list_by_subject(&self.partition_id, subject, None, &ConsistencyMode::MinimizeLatency)?; + let active_tuples = self.engine.storage().list_by_subject( + &self.partition_id, + subject, + None, + &ConsistencyMode::MinimizeLatency, + )?; // Query audit entries in pages to avoid OOM, filter by subject const PAGE_SIZE: u64 = 1000; let mut audit_entries = Vec::new(); let mut cursor: Option = None; loop { - let page = self - .engine - .storage() - .query_audit(&self.partition_id, None, None, None, &PaginationParams { + let page = self.engine.storage().query_audit( + &self.partition_id, + None, + None, + None, + &PaginationParams { limit: PAGE_SIZE, cursor, - })?; + }, + )?; let count_before = audit_entries.len(); audit_entries.extend(page.into_iter().filter(|e| e.subject == subject.as_str())); if audit_entries.len() - count_before < PAGE_SIZE as usize { @@ -225,18 +251,19 @@ impl<'a> GdprManager<'a> { /// /// Removes all tuples and audit entries involving the subject. pub fn right_to_erasure(&self, subject: &SubjectId) -> AegisResult<()> { - self.engine.storage().delete_subject(&self.partition_id, subject)?; + self.engine + .storage() + .delete_subject(&self.partition_id, subject)?; Ok(()) } fn delete_events_before(&self, cutoff: DateTime) -> AegisResult { - self.engine.storage().delete_events_before(&self.partition_id, cutoff) + self.engine + .storage() + .delete_events_before(&self.partition_id, cutoff) } - fn delete_soft_deleted_tuples_before( - &self, - cutoff: DateTime, - ) -> AegisResult { + fn delete_soft_deleted_tuples_before(&self, cutoff: DateTime) -> AegisResult { self.engine .storage() .delete_soft_deleted_tuples_before(&self.partition_id, cutoff) @@ -255,9 +282,9 @@ impl<'a> GdprManager<'a> { #[cfg(all(test, feature = "sqlite"))] mod tests { use super::*; + use crate::storage::StorageBackend; #[cfg(feature = "sqlite")] use crate::storage::sqlite::{SqliteConfig, SqliteStorage}; - use crate::storage::StorageBackend; use crate::types::*; fn make_engine_and_partition() -> (GraphEngine, PartitionId) { @@ -360,7 +387,12 @@ mod tests { let tuples = engine .storage() - .list_by_subject(&partition_id, &subject, None, &ConsistencyMode::MinimizeLatency) + .list_by_subject( + &partition_id, + &subject, + None, + &ConsistencyMode::MinimizeLatency, + ) .unwrap(); assert_eq!(tuples.len(), 0); } @@ -417,7 +449,10 @@ mod tests { assert_eq!(export_alice.active_tuples.len(), 0); // Bob now has the tuple - let bob_tuples = engine.storage().list_by_subject(&partition_id, &bob, None, &ConsistencyMode::MinimizeLatency).unwrap(); + let bob_tuples = engine + .storage() + .list_by_subject(&partition_id, &bob, None, &ConsistencyMode::MinimizeLatency) + .unwrap(); assert_eq!(bob_tuples.len(), 1); assert_eq!(bob_tuples[0].object.as_str(), "repo:fluxbus"); assert_eq!(bob_tuples[0].relation.as_str(), "owner"); @@ -520,7 +555,15 @@ mod tests { .unwrap(); assert!(result.revision.as_u64() > 0); - let tuples = engine.storage().list_by_subject(&partition_id, &subject, None, &ConsistencyMode::MinimizeLatency).unwrap(); + let tuples = engine + .storage() + .list_by_subject( + &partition_id, + &subject, + None, + &ConsistencyMode::MinimizeLatency, + ) + .unwrap(); assert_eq!(tuples.len(), 0); } @@ -551,10 +594,7 @@ mod tests { .delete_subject_with_policy(&subject, "fail", None) .unwrap_err(); assert!( - match err { - crate::error::AegisError::OperationNotPermitted(_) => true, - _ => false, - }, + matches!(err, crate::error::AegisError::OperationNotPermitted(_)), "expected OperationNotPermitted, got {:?}", err ); diff --git a/crates/aegis-core/src/engine/hierarchy.rs b/crates/aegis-core/src/engine/hierarchy.rs index 3dbd7f0..7e4d316 100644 --- a/crates/aegis-core/src/engine/hierarchy.rs +++ b/crates/aegis-core/src/engine/hierarchy.rs @@ -105,18 +105,35 @@ mod tests { let team = ResourceId::new("team:eng").unwrap(); let repo = ResourceId::new("repo:fluxbus").unwrap(); - storage.write_tuple(&partition_id, &RelationshipTuple::new( - SubjectId::new("org:acme").unwrap(), - Relation::new("parent").unwrap(), - team.clone(), - )).unwrap(); - storage.write_tuple(&partition_id, &RelationshipTuple::new( - SubjectId::new("team:eng").unwrap(), - Relation::new("parent").unwrap(), - repo.clone(), - )).unwrap(); - - let ancestors = get_ancestors(&partition_id, storage.as_ref(), &repo, &Relation::new("parent").unwrap(), &consistency).unwrap(); + storage + .write_tuple( + &partition_id, + &RelationshipTuple::new( + SubjectId::new("org:acme").unwrap(), + Relation::new("parent").unwrap(), + team.clone(), + ), + ) + .unwrap(); + storage + .write_tuple( + &partition_id, + &RelationshipTuple::new( + SubjectId::new("team:eng").unwrap(), + Relation::new("parent").unwrap(), + repo.clone(), + ), + ) + .unwrap(); + + let ancestors = get_ancestors( + &partition_id, + storage.as_ref(), + &repo, + &Relation::new("parent").unwrap(), + &consistency, + ) + .unwrap(); assert!(ancestors.contains(&team)); assert!(ancestors.contains(&org)); } @@ -128,18 +145,35 @@ mod tests { let team = ResourceId::new("team:eng").unwrap(); let repo = ResourceId::new("repo:fluxbus").unwrap(); - storage.write_tuple(&partition_id, &RelationshipTuple::new( - SubjectId::new("org:acme").unwrap(), - Relation::new("parent").unwrap(), - team.clone(), - )).unwrap(); - storage.write_tuple(&partition_id, &RelationshipTuple::new( - SubjectId::new("team:eng").unwrap(), - Relation::new("parent").unwrap(), - repo.clone(), - )).unwrap(); - - let descendants = get_descendants(&partition_id, storage.as_ref(), &org, &Relation::new("parent").unwrap(), &consistency).unwrap(); + storage + .write_tuple( + &partition_id, + &RelationshipTuple::new( + SubjectId::new("org:acme").unwrap(), + Relation::new("parent").unwrap(), + team.clone(), + ), + ) + .unwrap(); + storage + .write_tuple( + &partition_id, + &RelationshipTuple::new( + SubjectId::new("team:eng").unwrap(), + Relation::new("parent").unwrap(), + repo.clone(), + ), + ) + .unwrap(); + + let descendants = get_descendants( + &partition_id, + storage.as_ref(), + &org, + &Relation::new("parent").unwrap(), + &consistency, + ) + .unwrap(); assert!(descendants.contains(&team)); assert!(descendants.contains(&repo)); } @@ -150,13 +184,38 @@ mod tests { let org = ResourceId::new("org:acme").unwrap(); let repo = ResourceId::new("repo:fluxbus").unwrap(); - storage.write_tuple(&partition_id, &RelationshipTuple::new( - SubjectId::new("org:acme").unwrap(), - Relation::new("parent").unwrap(), - repo.clone(), - )).unwrap(); - - assert!(is_ancestor(&partition_id, storage.as_ref(), &org, &repo, &Relation::new("parent").unwrap(), &consistency).unwrap()); - assert!(!is_ancestor(&partition_id, storage.as_ref(), &repo, &org, &Relation::new("parent").unwrap(), &consistency).unwrap()); + storage + .write_tuple( + &partition_id, + &RelationshipTuple::new( + SubjectId::new("org:acme").unwrap(), + Relation::new("parent").unwrap(), + repo.clone(), + ), + ) + .unwrap(); + + assert!( + is_ancestor( + &partition_id, + storage.as_ref(), + &org, + &repo, + &Relation::new("parent").unwrap(), + &consistency + ) + .unwrap() + ); + assert!( + !is_ancestor( + &partition_id, + storage.as_ref(), + &repo, + &org, + &Relation::new("parent").unwrap(), + &consistency + ) + .unwrap() + ); } } diff --git a/crates/aegis-core/src/engine/hot_reload.rs b/crates/aegis-core/src/engine/hot_reload.rs index fdfd5a1..d6ff632 100644 --- a/crates/aegis-core/src/engine/hot_reload.rs +++ b/crates/aegis-core/src/engine/hot_reload.rs @@ -69,9 +69,10 @@ impl SchemaWatcher { let existing = engine.schema(); let report = crate::engine::migration::check_compatibility(&existing, &new_schema); if !report.compatible { - return Err(crate::error::AegisError::SchemaValidation( - format!("incompatible schema change: {}", report.breaking.join(", ")), - )); + return Err(crate::error::AegisError::SchemaValidation(format!( + "incompatible schema change: {}", + report.breaking.join(", ") + ))); } } diff --git a/crates/aegis-core/src/engine/migration.rs b/crates/aegis-core/src/engine/migration.rs index 36f5874..4ad9f86 100644 --- a/crates/aegis-core/src/engine/migration.rs +++ b/crates/aegis-core/src/engine/migration.rs @@ -1,9 +1,10 @@ use crate::error::{AegisError, AegisResult}; use crate::storage::StorageBackend; -use crate::types::schema::{Schema, SchemaCompatibilityReport}; use crate::types::MigrationResult; +use crate::types::schema::{Schema, SchemaCompatibilityReport}; /// A single schema migration step. +#[allow(clippy::type_complexity)] pub struct MigrationStep { pub version: u32, pub description: String, @@ -27,6 +28,7 @@ impl MigrationRunner { } /// Register a migration step. + #[allow(clippy::type_complexity)] pub fn register( &mut self, version: u32, @@ -100,7 +102,10 @@ impl MigrationRunner { step.version, step.description, e )) })?; - applied.push(format!("V{}: {} (rolled back)", step.version, step.description)); + applied.push(format!( + "V{}: {} (rolled back)", + step.version, step.description + )); } Ok(MigrationResult { @@ -153,10 +158,7 @@ pub fn register_default_migrations(runner: &mut MigrationRunner) { } /// Check schema compatibility between an existing and new schema. -pub fn check_compatibility( - existing: &Schema, - new_schema: &Schema, -) -> SchemaCompatibilityReport { +pub fn check_compatibility(existing: &Schema, new_schema: &Schema) -> SchemaCompatibilityReport { let mut warnings = Vec::new(); let mut breaking = Vec::new(); @@ -172,10 +174,7 @@ pub fn check_compatibility( if let Some(new_type) = new_schema.types.get(type_name) { for rel_name in type_def.relations.keys() { if !new_type.relations.contains_key(rel_name) { - breaking.push(format!( - "removed relation '{}.{}'", - type_name, rel_name - )); + breaking.push(format!("removed relation '{}.{}'", type_name, rel_name)); } } } @@ -186,10 +185,7 @@ pub fn check_compatibility( if let Some(new_type) = new_schema.types.get(type_name) { for perm_name in type_def.permissions.keys() { if !new_type.permissions.contains_key(perm_name) { - warnings.push(format!( - "removed permission '{}.{}'", - type_name, perm_name - )); + warnings.push(format!("removed permission '{}.{}'", type_name, perm_name)); } } } @@ -207,10 +203,7 @@ pub fn check_compatibility( if let Some(existing_type) = existing.types.get(type_name) { for rel_name in type_def.relations.keys() { if !existing_type.relations.contains_key(rel_name) { - warnings.push(format!( - "new relation '{}.{}' added", - type_name, rel_name - )); + warnings.push(format!("new relation '{}.{}' added", type_name, rel_name)); } } } @@ -299,7 +292,11 @@ mod tests { repo_perms.insert( "read".to_string(), PermissionDef { - union_of: vec!["viewer".to_string(), "editor".to_string(), "owner".to_string()], + union_of: vec![ + "viewer".to_string(), + "editor".to_string(), + "owner".to_string(), + ], condition: None, description: None, ..Default::default() diff --git a/crates/aegis-core/src/engine/mod.rs b/crates/aegis-core/src/engine/mod.rs index df08f25..9c46c7f 100644 --- a/crates/aegis-core/src/engine/mod.rs +++ b/crates/aegis-core/src/engine/mod.rs @@ -2,46 +2,48 @@ pub mod acl; pub mod analysis; pub mod cache; pub mod condition; +pub mod enforcement_history; pub mod gdpr; pub mod hierarchy; pub mod hooks; -pub mod partition; -pub mod enforcement_history; -pub mod policy_lifecycle; #[cfg(feature = "hot-reload")] pub mod hot_reload; pub mod migration; +pub mod partition; pub mod policy; +pub mod policy_lifecycle; pub mod ratelimit; -pub mod scheduler; pub mod rbac; +pub mod scheduler; pub mod traversal; pub mod watch; -use chrono::Utc; use crate::engine::cache::{DecisionCache, TraversalCache}; +#[cfg(feature = "hot-reload")] +use crate::engine::hot_reload::SchemaWatcher; use crate::engine::migration::MigrationRunner; use crate::engine::partition::PartitionManager; use crate::engine::ratelimit::{RateLimitOp, TokenBucketRateLimiter}; -use crate::engine::watch::{SharedWatchers, WatchEvent, WatchEventType, WatchFilter, WatchSubscription}; +use crate::engine::watch::{ + SharedWatchers, WatchEvent, WatchEventType, WatchFilter, WatchSubscription, +}; use crate::error::{AegisError, AegisResult}; use crate::storage::{StorageBackend, StorageTransaction, TupleFilter}; +use crate::types::schema::SchemaCompatibilityReport; use crate::types::{ AccessReviewEntry, CheckResult, ConsistencyMode, ExplainResult, ExplainTrace, FailClosedMode, - HealthReport, MigrationResult, PartitionId, Relation, RelationshipTuple, ResourceId, Revision, - RevisionToken, Schema, SubjectId, PaginatedTuples, PaginationParams, + HealthReport, MigrationResult, PaginatedTuples, PaginationParams, PartitionId, Relation, + RelationshipTuple, ResourceId, Revision, RevisionToken, Schema, SubjectId, }; -use crate::types::schema::SchemaCompatibilityReport; -#[cfg(feature = "hot-reload")] -use crate::engine::hot_reload::SchemaWatcher; +use chrono::Utc; +use sha2::{Digest, Sha256}; use std::collections::HashMap; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex, RwLock}; #[cfg(feature = "hot-reload")] use std::thread::JoinHandle; -use sha2::{Digest, Sha256}; use subtle::ConstantTimeEq; -use tracing::{error, field, info, span, Level}; +use tracing::{Level, error, field, info, span}; /// The core authorization engine. /// @@ -79,12 +81,16 @@ pub struct GraphEngine { wal_checkpoint_threshold: Option, #[cfg(feature = "async-storage")] async_storage: Option>, - analysis_cache: std::sync::Mutex>, + analysis_cache: + std::sync::Mutex>, drafts: std::sync::Mutex>, - pub(crate) analysis_schedules: std::sync::Mutex>, - pub(crate) analysis_runs: std::sync::Mutex>, + pub(crate) analysis_schedules: + std::sync::Mutex>, + pub(crate) analysis_runs: + std::sync::Mutex>, pub(crate) enforcement_config: std::sync::Mutex, - pub(crate) enforcement_events: std::sync::Mutex>, + pub(crate) enforcement_events: + std::sync::Mutex>, pub(crate) enforcement_rate_tracker: std::sync::Mutex, } @@ -126,7 +132,9 @@ impl GraphEngine { schema_watcher: Mutex::new(None), #[cfg(feature = "hot-reload")] watcher_thread: Mutex::new(None), - rate_limiter: Mutex::new(TokenBucketRateLimiter::new(ratelimit::RateLimitConfig::default())), + rate_limiter: Mutex::new(TokenBucketRateLimiter::new( + ratelimit::RateLimitConfig::default(), + )), telemetry_enabled: std::sync::atomic::AtomicBool::new(false), api_key_hash: None, api_key_verified: AtomicBool::new(false), @@ -143,9 +151,13 @@ impl GraphEngine { drafts: std::sync::Mutex::new(std::collections::HashMap::new()), analysis_schedules: std::sync::Mutex::new(std::collections::HashMap::new()), analysis_runs: std::sync::Mutex::new(std::collections::HashMap::new()), - enforcement_config: std::sync::Mutex::new(enforcement_history::EnforcementHistoryConfig::default()), + enforcement_config: std::sync::Mutex::new( + enforcement_history::EnforcementHistoryConfig::default(), + ), enforcement_events: std::sync::Mutex::new(std::collections::VecDeque::new()), - enforcement_rate_tracker: std::sync::Mutex::new(enforcement_history::RateTracker::new(10_000)), + enforcement_rate_tracker: std::sync::Mutex::new(enforcement_history::RateTracker::new( + 10_000, + )), } } @@ -192,12 +204,17 @@ impl GraphEngine { /// Verify an API key against the configured key (if any). /// Returns `true` if no API key is configured or if it matches. pub fn verify_api_key(&self, api_key: &str) -> bool { - let Some(configured_hash) = self.api_key_hash else { return true; }; + let Some(configured_hash) = self.api_key_hash else { + return true; + }; let mut hasher = Sha256::new(); hasher.update(api_key.as_bytes()); let result = hasher.finalize(); let incoming = u64::from_le_bytes(result[..8].try_into().unwrap()); - configured_hash.to_le_bytes().ct_eq(&incoming.to_le_bytes()).into() + configured_hash + .to_le_bytes() + .ct_eq(&incoming.to_le_bytes()) + .into() } /// Authenticate the engine with an API key for subsequent write/delete operations. @@ -248,6 +265,7 @@ impl GraphEngine { /// Emit a structured log event through the registered callback (if any). fn emit_log(&self, level: hooks::LogLevel, message: &str, context: &str) { + #[allow(clippy::collapsible_if)] if let Ok(guard) = self.logger.lock() { if let Some(ref logger) = *guard { logger(level, message, context); @@ -315,7 +333,9 @@ impl GraphEngine { timestamp: chrono::Utc::now(), payload, }; - let Ok(mut watchers) = self.watchers.lock() else { return }; + let Ok(mut watchers) = self.watchers.lock() else { + return; + }; watchers.retain(|_, (filter, tx)| { if !filter.matches(&event) { return true; @@ -333,7 +353,10 @@ impl GraphEngine { /// Set a custom MeterProvider for OpenTelemetry metrics. /// When set, this provider is used instead of the global meter provider. #[cfg(feature = "telemetry")] - pub fn with_meter_provider(self, provider: opentelemetry_sdk::metrics::SdkMeterProvider) -> Self { + pub fn with_meter_provider( + self, + provider: opentelemetry_sdk::metrics::SdkMeterProvider, + ) -> Self { crate::telemetry::otel_metrics::init_provider(provider); self } @@ -375,14 +398,19 @@ impl GraphEngine { /// Requires the `hot-reload` feature. #[cfg(feature = "hot-reload")] pub fn check_schema_reload(&self) -> AegisResult { - let watcher = self.schema_watcher.lock().map_err(|e| { - AegisError::Internal(format!("schema watcher lock failed: {e}")) - })?; + let watcher = self + .schema_watcher + .lock() + .map_err(|e| AegisError::Internal(format!("schema watcher lock failed: {e}")))?; match watcher.as_ref() { Some(w) => { let reloaded = w.check_and_reload(self)?; if reloaded { - self.emit_log(hooks::LogLevel::Info, "Schema hot-reloaded", "(schema file changed)"); + self.emit_log( + hooks::LogLevel::Info, + "Schema hot-reloaded", + "(schema file changed)", + ); } Ok(reloaded) } @@ -395,7 +423,9 @@ impl GraphEngine { /// Requires the `hot-reload` feature. #[cfg(feature = "hot-reload")] pub fn start_background_poller(self: &Arc) { - let Ok(mut guard) = self.watcher_thread.lock() else { return }; + let Ok(mut guard) = self.watcher_thread.lock() else { + return; + }; if guard.is_some() { return; } @@ -429,6 +459,7 @@ impl GraphEngine { #[cfg(feature = "hot-reload")] pub fn stop_watcher(&self) { self.shutdown_flag.store(true, Ordering::Relaxed); + #[allow(clippy::collapsible_if)] if let Ok(mut guard) = self.watcher_thread.lock() { if let Some(handle) = guard.take() { handle.join().ok(); @@ -439,14 +470,19 @@ impl GraphEngine { pub fn with_partition(&self, partition_id: PartitionId) -> AegisResult<()> { // Validate partition exists self.partition_manager.get_or_create(&partition_id)?; - *self.active_partition.write().map_err(|_| { - crate::error::AegisError::Internal("partition lock poisoned".into()) - })? = partition_id; + *self + .active_partition + .write() + .map_err(|_| crate::error::AegisError::Internal("partition lock poisoned".into()))? = + partition_id; Ok(()) } pub fn active_partition_id(&self) -> PartitionId { - self.active_partition.read().map(|p| p.clone()).unwrap_or_default() + self.active_partition + .read() + .map(|p| p.clone()) + .unwrap_or_default() } fn with_cache(&self, f: F) -> Option @@ -512,9 +548,14 @@ impl GraphEngine { /// Health check: returns a report of engine health. pub fn health(&self) -> HealthReport { - let revision = self.storage.current_revision(&self.active_partition_id()).ok(); + let revision = self + .storage + .current_revision(&self.active_partition_id()) + .ok(); let integrity = self.storage.integrity_check().ok(); - let cache_info = self.with_cache(|cache| (cache.hit_rate(), cache.len())).unwrap_or((0.0, 0)); + let cache_info = self + .with_cache(|cache| (cache.hit_rate(), cache.len())) + .unwrap_or((0.0, 0)); let schema = self.schema.read().unwrap_or_else(|e| e.into_inner()); // Update telemetry cache metrics @@ -526,7 +567,10 @@ impl GraphEngine { if i.passed { "ok".to_string() } else { - i.details.first().cloned().unwrap_or_else(|| "fail".to_string()) + i.details + .first() + .cloned() + .unwrap_or_else(|| "fail".to_string()) } }) .unwrap_or_else(|| "unknown".to_string()); @@ -544,16 +588,23 @@ impl GraphEngine { schema_version: schema.schema_version, backend: self.storage.backend_type().to_string(), backend_healthy: integrity.as_ref().map(|i| i.passed).unwrap_or(false), - telemetry_healthy: self.telemetry_enabled.load(std::sync::atomic::Ordering::Relaxed), + telemetry_healthy: self + .telemetry_enabled + .load(std::sync::atomic::Ordering::Relaxed), cache_hit_rate: cache_info.0, cache_entries: cache_info.1, storage_integrity: integrity.as_ref().map(|i| i.passed).unwrap_or(false), error: None, - total_checks: crate::telemetry::METRIC_CHECK_TOTAL.load(std::sync::atomic::Ordering::Relaxed), - allowed_checks: crate::telemetry::METRIC_CHECK_ALLOWED.load(std::sync::atomic::Ordering::Relaxed), - denied_checks: crate::telemetry::METRIC_CHECK_DENIED.load(std::sync::atomic::Ordering::Relaxed), - error_checks: crate::telemetry::METRIC_CHECK_ERROR.load(std::sync::atomic::Ordering::Relaxed), - cache_size: crate::telemetry::METRIC_CACHE_SIZE.load(std::sync::atomic::Ordering::Relaxed), + total_checks: crate::telemetry::METRIC_CHECK_TOTAL + .load(std::sync::atomic::Ordering::Relaxed), + allowed_checks: crate::telemetry::METRIC_CHECK_ALLOWED + .load(std::sync::atomic::Ordering::Relaxed), + denied_checks: crate::telemetry::METRIC_CHECK_DENIED + .load(std::sync::atomic::Ordering::Relaxed), + error_checks: crate::telemetry::METRIC_CHECK_ERROR + .load(std::sync::atomic::Ordering::Relaxed), + cache_size: crate::telemetry::METRIC_CACHE_SIZE + .load(std::sync::atomic::Ordering::Relaxed), cache_hit_ratio: cache_info.0, integrity_status, uptime_ms, @@ -568,8 +619,14 @@ impl GraphEngine { /// Returns the latest revision after recovery. /// Only meaningful for backends that persist an event log (e.g. SQLite). pub fn recover_from_events(&self, to_revision: Option) -> AegisResult { - let rev = self.storage.recover_from_events(&self.active_partition_id(), to_revision)?; - self.emit_log(hooks::LogLevel::Info, "Recovered from event log", &format!("revision={}", rev)); + let rev = self + .storage + .recover_from_events(&self.active_partition_id(), to_revision)?; + self.emit_log( + hooks::LogLevel::Info, + "Recovered from event log", + &format!("revision={}", rev), + ); Ok(rev) } @@ -606,7 +663,7 @@ impl GraphEngine { let rev = Some(revision); let mut cache_guard = self.traversal_cache.lock().ok(); let cache_ref = cache_guard.as_deref_mut(); - let result = match traversal::bfs_traversal_with_limits_and_context( + let result = traversal::bfs_traversal_with_limits_and_context( &self.active_partition_id(), self.storage.as_ref(), subject, @@ -619,10 +676,7 @@ impl GraphEngine { cache_ref, Some(&context), None, - ) { - Ok(r) => r, - Err(e) => return Err(e), - }; + )?; if result.found && evaluate_condition_if_present(&condition_str, &context) { return Ok(true); @@ -652,9 +706,7 @@ impl GraphEngine { let rel_names: Vec = resolved.relations.clone(); let condition_str = std::sync::Arc::new(resolved.condition); let ctx = std::sync::Arc::new(context); - let subject = Arc::new(SubjectId::new(subject.as_str()).map_err(|e| { - AegisError::Validation(e) - })?); + let subject = Arc::new(SubjectId::new(subject.as_str()).map_err(AegisError::Validation)?); std::thread::scope(|s| { for rel_name in &rel_names { @@ -689,8 +741,10 @@ impl GraphEngine { Some(ctx_ref.as_ref()), None, ); + #[allow(clippy::collapsible_if)] if let Ok(r) = result { - if r.found && evaluate_condition_if_present(cond.as_ref(), ctx_ref.as_ref()) { + if r.found && evaluate_condition_if_present(cond.as_ref(), ctx_ref.as_ref()) + { found_ref.store(true, std::sync::atomic::Ordering::Relaxed); } } @@ -735,7 +789,14 @@ impl GraphEngine { consistency: Option, context: condition::ConditionEvalContext, ) -> AegisResult { - self.check_inner(subject, permission, resource, consistency, false, Some(context)) + self.check_inner( + subject, + permission, + resource, + consistency, + false, + Some(context), + ) } /// Dry-run check with ABAC context. @@ -747,7 +808,14 @@ impl GraphEngine { consistency: Option, context: condition::ConditionEvalContext, ) -> AegisResult { - self.check_inner(subject, permission, resource, consistency, true, Some(context)) + self.check_inner( + subject, + permission, + resource, + consistency, + true, + Some(context), + ) } /// Async check: evaluate authorization using the async storage backend. @@ -780,30 +848,48 @@ impl GraphEngine { let _start = std::time::Instant::now(); let rl_key = format!("async_check:{}", resource.as_str()); - if let Err(e) = self.rate_limiter.lock().unwrap().check(&rl_key, RateLimitOp::Check) { + if let Err(e) = self + .rate_limiter + .lock() + .unwrap() + .check(&rl_key, RateLimitOp::Check) + { crate::telemetry::inc_check_error(); return Err(e); } - let storage = self.async_storage.as_ref().ok_or_else(|| { - AegisError::Internal("async storage not configured".to_string()) - })?; + let storage = self + .async_storage + .as_ref() + .ok_or_else(|| AegisError::Internal("async storage not configured".to_string()))?; let revision = match consistency { Some(ConsistencyMode::AtRevision(rev)) => { - let current = storage.current_revision(&self.active_partition_id()).await?; + let current = storage + .current_revision(&self.active_partition_id()) + .await?; if rev > current { return Err(AegisError::RevisionFromFuture(rev.as_u64() as usize)); } rev } - _ => storage.current_revision(&self.active_partition_id()).await?, + _ => { + storage + .current_revision(&self.active_partition_id()) + .await? + } }; // Cache check let pid = self.active_partition_id(); let from_cache = self.with_cache(|cache| { - cache.get(subject.as_str(), permission, resource.as_str(), pid.as_str(), revision) + cache.get( + subject.as_str(), + permission, + resource.as_str(), + pid.as_str(), + revision, + ) }); if let Some(Some(allowed)) = from_cache { info!( @@ -814,23 +900,32 @@ impl GraphEngine { ); crate::telemetry::inc_cache_hit(); crate::telemetry::inc_check_total(); - if allowed { crate::telemetry::inc_check_allowed(); } - else { crate::telemetry::inc_check_denied(); } + if allowed { + crate::telemetry::inc_check_allowed(); + } else { + crate::telemetry::inc_check_denied(); + } return Ok(CheckResult { allowed, revision }); } // Resolve permission to relations let resource_type = resource_type_name(resource.as_str()); - let schema = self.schema.read().unwrap(); - let resolved = match policy::resolve_permission(&schema, &resource_type, permission) { - Some(r) => r, - None => { - crate::telemetry::inc_check_total(); - crate::telemetry::inc_check_denied(); - return Ok(CheckResult { allowed: false, revision }); - } + let resolved = { + let schema = self.schema.read().unwrap(); + #[allow(clippy::let_and_return)] + let result = match policy::resolve_permission(&schema, &resource_type, permission) { + Some(r) => r, + None => { + crate::telemetry::inc_check_total(); + crate::telemetry::inc_check_denied(); + return Ok(CheckResult { + allowed: false, + revision, + }); + } + }; + result }; - drop(schema); // Evaluate each candidate relation by checking tuples from async storage let mut allowed = false; @@ -839,12 +934,14 @@ impl GraphEngine { Ok(r) => r, Err(_) => continue, }; - let tuples = storage.list_by_object( - &self.active_partition_id(), - resource, - Some(&rel), - &ConsistencyMode::AtRevision(revision), - ).await?; + let tuples = storage + .list_by_object( + &self.active_partition_id(), + resource, + Some(&rel), + &ConsistencyMode::AtRevision(revision), + ) + .await?; for t in &tuples { if t.subject.as_str() == subject.as_str() { @@ -858,8 +955,11 @@ impl GraphEngine { } crate::telemetry::inc_check_total(); - if allowed { crate::telemetry::inc_check_allowed(); } - else { crate::telemetry::inc_check_denied(); } + if allowed { + crate::telemetry::inc_check_allowed(); + } else { + crate::telemetry::inc_check_denied(); + } if !allowed { self.with_cache(|cache| { @@ -872,7 +972,10 @@ impl GraphEngine { revision, ); }); - return Ok(CheckResult { allowed: false, revision }); + return Ok(CheckResult { + allowed: false, + revision, + }); } self.with_cache(|cache| { @@ -886,7 +989,10 @@ impl GraphEngine { ); }); - Ok(CheckResult { allowed: true, revision }) + Ok(CheckResult { + allowed: true, + revision, + }) } /// Internal check implementation with dry_run flag. @@ -913,7 +1019,12 @@ impl GraphEngine { // Rate limit check let rl_key = format!("check:{}", resource.as_str()); - if let Err(e) = self.rate_limiter.lock().unwrap().check(&rl_key, RateLimitOp::Check) { + if let Err(e) = self + .rate_limiter + .lock() + .unwrap() + .check(&rl_key, RateLimitOp::Check) + { crate::telemetry::inc_check_error(); return Err(e); } @@ -932,7 +1043,13 @@ impl GraphEngine { let _cache_guard = cache_span.enter(); let pid = self.active_partition_id(); let from_cache = self.with_cache(|cache| { - cache.get(subject.as_str(), permission, resource.as_str(), pid.as_str(), revision) + cache.get( + subject.as_str(), + permission, + resource.as_str(), + pid.as_str(), + revision, + ) }); if let Some(Some(allowed)) = from_cache { info!( @@ -975,22 +1092,33 @@ impl GraphEngine { // Parallel evaluation via scoped threads when enabled. let has_context = context.is_some(); let ctx = context.unwrap_or_default(); - let mut allowed = match if self.parallel_eval.load(Ordering::Relaxed) && resolved.relations.len() > 1 { - self.evaluate_relations_parallel( - resolved, subject, resource, revision, consistency, ctx, - ) - } else { - self.evaluate_relations_sequential( - resolved, subject, resource, revision, consistency, ctx, - ) - } { - Ok(a) => a, - Err(e) => { - crate::telemetry::inc_check_error(); - crate::telemetry::inc_check_total(); - return self.fail_closed_response(e); - } - }; + let mut allowed = + match if self.parallel_eval.load(Ordering::Relaxed) && resolved.relations.len() > 1 { + self.evaluate_relations_parallel( + resolved, + subject, + resource, + revision, + consistency, + ctx, + ) + } else { + self.evaluate_relations_sequential( + resolved, + subject, + resource, + revision, + consistency, + ctx, + ) + } { + Ok(a) => a, + Err(e) => { + crate::telemetry::inc_check_error(); + crate::telemetry::inc_check_total(); + return self.fail_closed_response(e); + } + }; // Permission-level Deny effect: if the permission has Effect::Deny, // finding any match in the union_of relations denies instead of allows. @@ -1018,6 +1146,7 @@ impl GraphEngine { Some(revision), consistency, ); + #[allow(clippy::collapsible_if)] if let Ok(tr) = traversal_result { if tr.found { allowed = false; @@ -1034,7 +1163,14 @@ impl GraphEngine { // Cache the decision let pid = self.active_partition_id(); self.with_cache(|cache| { - cache.insert(subject.as_str(), permission, resource.as_str(), pid.as_str(), allowed, revision); + cache.insert( + subject.as_str(), + permission, + resource.as_str(), + pid.as_str(), + allowed, + revision, + ); }); self.hooks.trigger(&hooks::HookEvent::OnCheck { @@ -1164,6 +1300,7 @@ impl GraphEngine { Some(revision), consistency, ); + #[allow(clippy::collapsible_if)] if let Ok(tr) = tr { if tr.found { allowed = false; @@ -1216,7 +1353,10 @@ impl GraphEngine { // Rate limit check let rl_key = format!("write:{}", tuple.object.as_str()); - self.rate_limiter.lock().unwrap().check(&rl_key, RateLimitOp::Write)?; + self.rate_limiter + .lock() + .unwrap() + .check(&rl_key, RateLimitOp::Write)?; // Schema validation let resource_type = resource_type_name(tuple.object.as_str()); @@ -1234,7 +1374,9 @@ impl GraphEngine { drop(schema); self.storage.set_actor_identity(self.active_actor()); - let revision = self.storage.write_tuple(&self.active_partition_id(), tuple)?; + let revision = self + .storage + .write_tuple(&self.active_partition_id(), tuple)?; crate::telemetry::update_revision_current(revision.as_u64()); info!(revision = field::display(&revision), "tuple written"); @@ -1271,10 +1413,15 @@ impl GraphEngine { // Rate limit check let rl_key = format!("delete:{}", key.object.as_str()); - self.rate_limiter.lock().unwrap().check(&rl_key, RateLimitOp::Write)?; + self.rate_limiter + .lock() + .unwrap() + .check(&rl_key, RateLimitOp::Write)?; self.storage.set_actor_identity(self.active_actor()); - let revision = self.storage.delete_tuple(&self.active_partition_id(), key)?; + let revision = self + .storage + .delete_tuple(&self.active_partition_id(), key)?; crate::telemetry::update_revision_current(revision.as_u64()); info!(revision = field::display(&revision), "tuple deleted"); @@ -1287,9 +1434,8 @@ impl GraphEngine { revision, ); - self.hooks.trigger(&hooks::HookEvent::OnDelete { - key: key.clone(), - }); + self.hooks + .trigger(&hooks::HookEvent::OnDelete { key: key.clone() }); Ok(RevisionToken::new(revision, self.node_id)) } @@ -1314,31 +1460,41 @@ impl GraphEngine { self.maybe_checkpoint_wal(); let rl_key = format!("async_write:{}", tuple.object.as_str()); - self.rate_limiter.lock().unwrap().check(&rl_key, RateLimitOp::Write)?; + self.rate_limiter + .lock() + .unwrap() + .check(&rl_key, RateLimitOp::Write)?; let resource_type = resource_type_name(tuple.object.as_str()); - let schema = self.schema.read().unwrap(); - let type_def = match schema.types.get(&resource_type) { - Some(t) => t, - None => return Err(AegisError::UnknownSubjectType(resource_type)), - }; - if !type_def.relations.contains_key(tuple.relation.as_str()) { - return Err(AegisError::UnknownRelation { - type_name: resource_type, - relation: tuple.relation.to_string(), - }); + { + let schema = self.schema.read().unwrap(); + let type_def = match schema.types.get(&resource_type) { + Some(t) => t, + None => return Err(AegisError::UnknownSubjectType(resource_type)), + }; + if !type_def.relations.contains_key(tuple.relation.as_str()) { + return Err(AegisError::UnknownRelation { + type_name: resource_type, + relation: tuple.relation.to_string(), + }); + } } - drop(schema); - let storage = self.async_storage.as_ref().ok_or_else(|| { - AegisError::Internal("async storage not configured".to_string()) - })?; + let storage = self + .async_storage + .as_ref() + .ok_or_else(|| AegisError::Internal("async storage not configured".to_string()))?; storage.set_actor_identity(self.active_actor()).await; - let revision = storage.write_tuple(&self.active_partition_id(), tuple).await?; + let revision = storage + .write_tuple(&self.active_partition_id(), tuple) + .await?; crate::telemetry::update_revision_current(revision.as_u64()); - info!(revision = field::display(&revision), "tuple written (async)"); + info!( + revision = field::display(&revision), + "tuple written (async)" + ); self.emit_watch_event( WatchEventType::TupleAdded, @@ -1357,10 +1513,7 @@ impl GraphEngine { /// Async delete: delete a tuple by key using the async storage backend. #[cfg(feature = "async-storage")] - pub async fn async_delete( - &self, - key: &crate::types::TupleKey, - ) -> AegisResult { + pub async fn async_delete(&self, key: &crate::types::TupleKey) -> AegisResult { let _span = span!( Level::INFO, "aegis.async_delete", @@ -1375,17 +1528,26 @@ impl GraphEngine { self.maybe_checkpoint_wal(); let rl_key = format!("async_delete:{}", key.object.as_str()); - self.rate_limiter.lock().unwrap().check(&rl_key, RateLimitOp::Write)?; + self.rate_limiter + .lock() + .unwrap() + .check(&rl_key, RateLimitOp::Write)?; - let storage = self.async_storage.as_ref().ok_or_else(|| { - AegisError::Internal("async storage not configured".to_string()) - })?; + let storage = self + .async_storage + .as_ref() + .ok_or_else(|| AegisError::Internal("async storage not configured".to_string()))?; storage.set_actor_identity(self.active_actor()).await; - let revision = storage.delete_tuple(&self.active_partition_id(), key).await?; + let revision = storage + .delete_tuple(&self.active_partition_id(), key) + .await?; crate::telemetry::update_revision_current(revision.as_u64()); - info!(revision = field::display(&revision), "tuple deleted (async)"); + info!( + revision = field::display(&revision), + "tuple deleted (async)" + ); self.emit_watch_event( WatchEventType::TupleRemoved, @@ -1395,9 +1557,8 @@ impl GraphEngine { revision, ); - self.hooks.trigger(&hooks::HookEvent::OnDelete { - key: key.clone(), - }); + self.hooks + .trigger(&hooks::HookEvent::OnDelete { key: key.clone() }); Ok(RevisionToken::new(revision, self.node_id)) } @@ -1417,9 +1578,7 @@ impl GraphEngine { Some(ConsistencyMode::AtRevision(rev)) => { let current = self.storage.current_revision(&self.active_partition_id())?; if rev > current { - return Err(AegisError::RevisionFromFuture( - rev.as_u64() as usize, - )); + return Err(AegisError::RevisionFromFuture(rev.as_u64() as usize)); } Ok(rev) } @@ -1452,11 +1611,13 @@ impl GraphEngine { self.storage.save_policy_version(&save_ver)?; // Load the target version - let schema_json = self.storage.load_policy_version(version)? + let schema_json = self + .storage + .load_policy_version(version)? .ok_or_else(|| AegisError::Internal(format!("policy version {} not found", version)))?; - let new_schema: Schema = serde_json::from_str(&schema_json) - .map_err(|e| AegisError::Internal(e.to_string()))?; + let new_schema: Schema = + serde_json::from_str(&schema_json).map_err(|e| AegisError::Internal(e.to_string()))?; // Swap schema and update schema version { @@ -1497,7 +1658,13 @@ impl GraphEngine { to_revision: Option, pagination: &crate::types::PaginationParams, ) -> AegisResult> { - self.storage.query_audit(&self.active_partition_id(), Some(object), from_revision, to_revision, pagination) + self.storage.query_audit( + &self.active_partition_id(), + Some(object), + from_revision, + to_revision, + pagination, + ) } /// Query the audit log for all objects within an optional revision range. @@ -1507,16 +1674,33 @@ impl GraphEngine { to_revision: Option, pagination: &crate::types::PaginationParams, ) -> AegisResult> { - self.storage.query_audit(&self.active_partition_id(), None, from_revision, to_revision, pagination) + self.storage.query_audit( + &self.active_partition_id(), + None, + from_revision, + to_revision, + pagination, + ) } /// Export all tuples for a given subject (GDPR compliance). - pub fn export_subject(&self, subject: &SubjectId) -> AegisResult> { - self.storage.list_by_subject(&self.active_partition_id(), subject, None, &ConsistencyMode::MinimizeLatency) + pub fn export_subject( + &self, + subject: &SubjectId, + ) -> AegisResult> { + self.storage.list_by_subject( + &self.active_partition_id(), + subject, + None, + &ConsistencyMode::MinimizeLatency, + ) } /// Export signed subject data (GDPR Article 15 with cryptographic signature). - pub fn export_signed_subject_data(&self, subject: &SubjectId) -> AegisResult { + pub fn export_signed_subject_data( + &self, + subject: &SubjectId, + ) -> AegisResult { let gdpr = self.gdpr(); gdpr.sign_export(&gdpr.export_subject_data(subject)?) } @@ -1538,12 +1722,19 @@ impl GraphEngine { self.storage.set_actor_identity(self.active_actor()); match policy { "cascade" => { - let revision = self.storage.delete_subject(&self.active_partition_id(), subject)?; + let revision = self + .storage + .delete_subject(&self.active_partition_id(), subject)?; crate::telemetry::update_revision_current(revision.as_u64()); Ok(RevisionToken::new(revision, self.node_id)) } "fail" => { - let tuples = self.storage.list_by_subject(&self.active_partition_id(), subject, None, &ConsistencyMode::MinimizeLatency)?; + let tuples = self.storage.list_by_subject( + &self.active_partition_id(), + subject, + None, + &ConsistencyMode::MinimizeLatency, + )?; if tuples.is_empty() { let revision = self.storage.current_revision(&self.active_partition_id())?; crate::telemetry::update_revision_current(revision.as_u64()); @@ -1560,13 +1751,20 @@ impl GraphEngine { "transfer policy requires a transfer_to_subject".into(), ) })?; - let tuples = self.storage.list_by_subject(&self.active_partition_id(), subject, None, &ConsistencyMode::MinimizeLatency)?; + let tuples = self.storage.list_by_subject( + &self.active_partition_id(), + subject, + None, + &ConsistencyMode::MinimizeLatency, + )?; if tuples.is_empty() { let revision = self.storage.current_revision(&self.active_partition_id())?; crate::telemetry::update_revision_current(revision.as_u64()); return Ok(RevisionToken::new(revision, self.node_id)); } - let mut txn = self.storage.begin_transaction(&self.active_partition_id())?; + let mut txn = self + .storage + .begin_transaction(&self.active_partition_id())?; for tuple in &tuples { let new_tuple = RelationshipTuple { subject: target.clone(), @@ -1597,16 +1795,16 @@ impl GraphEngine { } /// Write multiple tuples atomically within a single transaction. - pub fn write_batch( - &self, - tuples: &[RelationshipTuple], - ) -> AegisResult { + pub fn write_batch(&self, tuples: &[RelationshipTuple]) -> AegisResult { let _span = span!(Level::INFO, "aegis.write_batch", count = tuples.len()).entered(); self.check_closed()?; self.check_authenticated()?; self.maybe_checkpoint_wal(); let rl_key = "write_batch"; - self.rate_limiter.lock().unwrap().check(rl_key, RateLimitOp::Write)?; + self.rate_limiter + .lock() + .unwrap() + .check(rl_key, RateLimitOp::Write)?; // Schema validation for each tuple let schema = self.schema.read().unwrap(); @@ -1626,7 +1824,9 @@ impl GraphEngine { drop(schema); self.storage.set_actor_identity(self.active_actor()); - let revision = self.storage.write_tuples_batch(&self.active_partition_id(), tuples)?; + let revision = self + .storage + .write_tuples_batch(&self.active_partition_id(), tuples)?; crate::telemetry::update_revision_current(revision.as_u64()); for tuple in tuples { self.emit_watch_event( @@ -1643,7 +1843,9 @@ impl GraphEngine { /// Begin a storage transaction for atomic multi-operation writes. pub fn transaction(&self) -> AegisResult> { - let mut txn = self.storage.begin_transaction(&self.active_partition_id())?; + let mut txn = self + .storage + .begin_transaction(&self.active_partition_id())?; if let Some(actor) = self.active_actor() { txn.set_actor_identity(Some(actor)); } @@ -1657,8 +1859,11 @@ impl GraphEngine { relation: Option<&Relation>, consistency: Option, ) -> AegisResult> { - let c = consistency.as_ref().unwrap_or(&ConsistencyMode::MinimizeLatency); - self.storage.list_by_object(&self.active_partition_id(), object, relation, c) + let c = consistency + .as_ref() + .unwrap_or(&ConsistencyMode::MinimizeLatency); + self.storage + .list_by_object(&self.active_partition_id(), object, relation, c) } /// List all tuples for a given subject, optionally filtered by relation. @@ -1668,8 +1873,11 @@ impl GraphEngine { relation: Option<&Relation>, consistency: Option, ) -> AegisResult> { - let c = consistency.as_ref().unwrap_or(&ConsistencyMode::MinimizeLatency); - self.storage.list_by_subject(&self.active_partition_id(), subject, relation, c) + let c = consistency + .as_ref() + .unwrap_or(&ConsistencyMode::MinimizeLatency); + self.storage + .list_by_subject(&self.active_partition_id(), subject, relation, c) } /// List all tuples matching a relation on an object. @@ -1678,7 +1886,8 @@ impl GraphEngine { object: &ResourceId, relation: &Relation, ) -> AegisResult> { - self.storage.list_by_relation(&self.active_partition_id(), object, relation) + self.storage + .list_by_relation(&self.active_partition_id(), object, relation) } /// Query tuples with filters and pagination. @@ -1688,16 +1897,16 @@ impl GraphEngine { pagination: &PaginationParams, consistency: Option, ) -> AegisResult { - let _span = span!( - Level::INFO, - crate::telemetry::spans::QUERY, - ) - .entered(); + let _span = span!(Level::INFO, crate::telemetry::spans::QUERY,).entered(); let consistency = consistency.unwrap_or_default(); let pagination = pagination.clone().capped(); - self.storage - .query_tuples(&self.active_partition_id(), filter, &pagination, &consistency) + self.storage.query_tuples( + &self.active_partition_id(), + filter, + &pagination, + &consistency, + ) } /// Run schema migrations to reach the target version. @@ -1720,11 +1929,21 @@ impl GraphEngine { pub fn delete_object(&self, object: &ResourceId) -> AegisResult { self.check_closed()?; self.check_authenticated()?; - let _span = span!(Level::INFO, "aegis.delete_object", resource = object.as_str()).entered(); + let _span = span!( + Level::INFO, + "aegis.delete_object", + resource = object.as_str() + ) + .entered(); let rl_key = format!("delete_object:{}", object.as_str()); - self.rate_limiter.lock().unwrap().check(&rl_key, RateLimitOp::Write)?; + self.rate_limiter + .lock() + .unwrap() + .check(&rl_key, RateLimitOp::Write)?; self.storage.set_actor_identity(self.active_actor()); - let revision = self.storage.delete_object(&self.active_partition_id(), object)?; + let revision = self + .storage + .delete_object(&self.active_partition_id(), object)?; crate::telemetry::update_revision_current(revision.as_u64()); info!(revision = field::display(&revision), "object deleted"); Ok(RevisionToken::new(revision, self.node_id)) @@ -1751,7 +1970,10 @@ impl GraphEngine { } /// Export all permissions for a given subject across all resources. - pub fn access_review_for_subject(&self, subject: &SubjectId) -> AegisResult> { + pub fn access_review_for_subject( + &self, + subject: &SubjectId, + ) -> AegisResult> { let tuples = self.storage.list_by_subject( &self.active_partition_id(), subject, @@ -1780,7 +2002,10 @@ impl GraphEngine { } /// Export all subjects with access to a given resource. - pub fn access_review_for_resource(&self, resource: &ResourceId) -> AegisResult> { + pub fn access_review_for_resource( + &self, + resource: &ResourceId, + ) -> AegisResult> { let tuples = self.storage.list_by_object( &self.active_partition_id(), resource, @@ -1822,9 +2047,11 @@ impl GraphEngine { /// Run integrity check if the configured interval has elapsed since the last check. pub fn ensure_integrity_check(&self) -> AegisResult<()> { let interval = *self.integrity_check_interval.read().unwrap(); - let Some(interval) = interval else { return Ok(()); }; + let Some(interval) = interval else { + return Ok(()); + }; let mut last = self.last_integrity_check.lock().unwrap(); - if last.map_or(true, |t| t.elapsed() >= interval) { + if last.is_none_or(|t| t.elapsed() >= interval) { let report = self.storage.integrity_check()?; if !report.passed { tracing::error!("integrity check failed: {:?}", report.details); @@ -1841,11 +2068,18 @@ impl GraphEngine { } fn maybe_checkpoint_wal(&self) { - let Some(threshold) = self.wal_checkpoint_threshold else { return; }; + let Some(threshold) = self.wal_checkpoint_threshold else { + return; + }; + #[allow(clippy::collapsible_if)] if let Some(wal_size) = self.storage.wal_size_mb() { if wal_size > threshold { let _ = self.storage.close(); - tracing::info!("WAL auto-checkpoint triggered ({} MB > {} MB)", wal_size, threshold); + tracing::info!( + "WAL auto-checkpoint triggered ({} MB > {} MB)", + wal_size, + threshold + ); } } } @@ -1887,11 +2121,11 @@ fn resource_type_name(id: &str) -> String { #[cfg(all(test, feature = "sqlite"))] mod tests { use super::*; + use crate::engine::acl; + use crate::engine::rbac; #[cfg(feature = "sqlite")] use crate::storage::sqlite::{SqliteConfig, SqliteStorage}; use crate::types::*; - use crate::engine::rbac; - use crate::engine::acl; fn make_engine() -> GraphEngine { let schema = Schema { schema_version: 1, @@ -1963,9 +2197,7 @@ mod tests { )) .unwrap(); - let result = engine - .check(&subject, "read", &resource, None) - .unwrap(); + let result = engine.check(&subject, "read", &resource, None).unwrap(); assert!(result.allowed); assert!(result.revision.as_u64() > 0); } @@ -1999,9 +2231,7 @@ mod tests { )) .unwrap(); - let result = engine - .check(&subject, "admin", &resource, None) - .unwrap(); + let result = engine.check(&subject, "admin", &resource, None).unwrap(); assert!(result.allowed); // viewer should NOT have admin @@ -2014,9 +2244,7 @@ mod tests { )) .unwrap(); - let result = engine - .check(&viewer, "admin", &resource, None) - .unwrap(); + let result = engine.check(&viewer, "admin", &resource, None).unwrap(); assert!(!result.allowed); } @@ -2034,9 +2262,7 @@ mod tests { )) .unwrap(); - let explain = engine - .explain(&subject, "read", &resource, None) - .unwrap(); + let explain = engine.explain(&subject, "read", &resource, None).unwrap(); assert!(explain.allowed); assert!(explain.revision.as_u64() > 0); } @@ -2070,16 +2296,12 @@ mod tests { .unwrap(); // First check populates cache - let result = engine - .check(&subject, "read", &resource, None) - .unwrap(); + let result = engine.check(&subject, "read", &resource, None).unwrap(); assert!(result.allowed); // Invalidate and verify still works (cache miss is fine) engine.invalidate_cache(); - let result = engine - .check(&subject, "read", &resource, None) - .unwrap(); + let result = engine.check(&subject, "read", &resource, None).unwrap(); assert!(result.allowed); } @@ -2108,9 +2330,7 @@ mod tests { // Disable parallel, verify check still works engine.set_parallel_eval(false); - let result = engine - .check(&subject, "read", &resource, None) - .unwrap(); + let result = engine.check(&subject, "read", &resource, None).unwrap(); assert!(result.allowed); } @@ -2133,7 +2353,12 @@ mod tests { // Read with FullyConsistent mode let result = engine - .check(&subject, "read", &resource, Some(ConsistencyMode::FullyConsistent)) + .check( + &subject, + "read", + &resource, + Some(ConsistencyMode::FullyConsistent), + ) .unwrap(); assert!(result.allowed); assert!(result.revision >= token.revision); @@ -2158,7 +2383,12 @@ mod tests { // Check at this revision — should be allowed (viewer can read) let result = engine - .check(&subject, "read", &resource, Some(ConsistencyMode::AtRevision(token1.revision))) + .check( + &subject, + "read", + &resource, + Some(ConsistencyMode::AtRevision(token1.revision)), + ) .unwrap(); assert!(result.allowed); } @@ -2178,7 +2408,11 @@ mod tests { // Trigger a close event engine.close().ok(); - engine.emit_log(crate::engine::hooks::LogLevel::Info, "test message", "test context"); + engine.emit_log( + crate::engine::hooks::LogLevel::Info, + "test message", + "test context", + ); let msgs = logged.lock().unwrap(); assert!(!msgs.is_empty(), "expected at least one log message"); @@ -2250,10 +2484,16 @@ types: #[test] fn test_empty_transaction() { let engine = make_engine(); - let rev_before = engine.storage().current_revision(&PartitionId::default()).unwrap(); + let rev_before = engine + .storage() + .current_revision(&PartitionId::default()) + .unwrap(); let txn = engine.transaction().unwrap(); let _rev = txn.commit().unwrap(); - let rev_after = engine.storage().current_revision(&PartitionId::default()).unwrap(); + let rev_after = engine + .storage() + .current_revision(&PartitionId::default()) + .unwrap(); assert_eq!(rev_before, rev_after); } @@ -2271,7 +2511,15 @@ types: txn.write(&PartitionId::default(), &tuple).unwrap(); let rev = txn.commit().unwrap(); assert!(rev.as_u64() > 0); - let tuples = engine.storage().list_by_object(&PartitionId::default(), &resource, None, &ConsistencyMode::MinimizeLatency).unwrap(); + let tuples = engine + .storage() + .list_by_object( + &PartitionId::default(), + &resource, + None, + &ConsistencyMode::MinimizeLatency, + ) + .unwrap(); assert_eq!(tuples.len(), 1); } @@ -2282,12 +2530,21 @@ types: let engine = make_engine(); let subject = SubjectId::new("user:1").unwrap(); let resource = ResourceId::new("repo:a").unwrap(); - let token = engine.write(&RelationshipTuple::new( - subject.clone(), - Relation::new("owner").unwrap(), - resource.clone(), - )).unwrap(); - let result = engine.check(&subject, "read", &resource, Some(ConsistencyMode::AtRevision(token.revision))).unwrap(); + let token = engine + .write(&RelationshipTuple::new( + subject.clone(), + Relation::new("owner").unwrap(), + resource.clone(), + )) + .unwrap(); + let result = engine + .check( + &subject, + "read", + &resource, + Some(ConsistencyMode::AtRevision(token.revision)), + ) + .unwrap(); assert!(result.allowed); } @@ -2341,8 +2598,16 @@ types: let engine = make_engine(); let subject = SubjectId::new("user:alice").unwrap(); let resource = ResourceId::new("repo:fluxbus").unwrap(); - engine.write(&RelationshipTuple::new(subject.clone(), Relation::new("owner").unwrap(), resource.clone())).unwrap(); - let dry = engine.check_dry_run(&subject, "read", &resource, None).unwrap(); + engine + .write(&RelationshipTuple::new( + subject.clone(), + Relation::new("owner").unwrap(), + resource.clone(), + )) + .unwrap(); + let dry = engine + .check_dry_run(&subject, "read", &resource, None) + .unwrap(); assert!(dry.allowed); assert!(dry.revision.as_u64() > 0); } @@ -2353,14 +2618,32 @@ types: // First write bumps revision so token.revision > 0 let dummy = SubjectId::new("user:dummy").unwrap(); let dummy_r = ResourceId::new("repo:dummy").unwrap(); - engine.write(&RelationshipTuple::new(dummy, Relation::new("owner").unwrap(), dummy_r)).unwrap(); + engine + .write(&RelationshipTuple::new( + dummy, + Relation::new("owner").unwrap(), + dummy_r, + )) + .unwrap(); let subject = SubjectId::new("user:dave").unwrap(); let resource = ResourceId::new("repo:dave").unwrap(); - let tuple = RelationshipTuple::new(subject.clone(), Relation::new("owner").unwrap(), resource.clone()); + let tuple = RelationshipTuple::new( + subject.clone(), + Relation::new("owner").unwrap(), + resource.clone(), + ); let token = engine.write_dry_run(&tuple).unwrap(); assert!(token.revision.as_u64() > 0); - let tuples = engine.storage().list_by_object(&PartitionId::default(), &resource, None, &ConsistencyMode::MinimizeLatency).unwrap(); + let tuples = engine + .storage() + .list_by_object( + &PartitionId::default(), + &resource, + None, + &ConsistencyMode::MinimizeLatency, + ) + .unwrap(); assert_eq!(tuples.len(), 0); } @@ -2369,7 +2652,11 @@ types: let engine = make_engine(); let subject = SubjectId::new("user:bad").unwrap(); let resource = ResourceId::new("repo:bad").unwrap(); - let tuple = RelationshipTuple::new(subject, Relation::new("nonexistent_relation").unwrap(), resource); + let tuple = RelationshipTuple::new( + subject, + Relation::new("nonexistent_relation").unwrap(), + resource, + ); let result = engine.write_dry_run(&tuple); assert!(result.is_err()); } @@ -2379,11 +2666,15 @@ types: let engine = make_engine(); let subject = SubjectId::new("user:carol").unwrap(); let resource = ResourceId::new("repo:carol").unwrap(); - let tuple = RelationshipTuple::new(subject.clone(), Relation::new("owner").unwrap(), resource.clone()); - engine.write(&tuple).unwrap(); - engine.check(&subject, "read", &resource, None).unwrap(); - let subject2 = SubjectId::new("user:other").unwrap(); - let resource2 = ResourceId::new("repo:other").unwrap(); + let tuple = RelationshipTuple::new( + subject.clone(), + Relation::new("owner").unwrap(), + resource.clone(), + ); + engine.write(&tuple).unwrap(); + engine.check(&subject, "read", &resource, None).unwrap(); + let subject2 = SubjectId::new("user:other").unwrap(); + let resource2 = ResourceId::new("repo:other").unwrap(); let tuple2 = RelationshipTuple::new(subject2, Relation::new("owner").unwrap(), resource2); engine.write_dry_run(&tuple2).unwrap(); let result = engine.check(&subject, "read", &resource, None).unwrap(); @@ -2399,16 +2690,54 @@ types: let r1 = ResourceId::new("repo:r1").unwrap(); let r2 = ResourceId::new("repo:r2").unwrap(); let r3 = ResourceId::new("repo:r3").unwrap(); - engine.write(&RelationshipTuple::new(subject.clone(), Relation::new("owner").unwrap(), r1.clone())).unwrap(); - engine.write(&RelationshipTuple::new(subject.clone(), Relation::new("viewer").unwrap(), r2.clone())).unwrap(); - engine.write(&RelationshipTuple::new(subject.clone(), Relation::new("owner").unwrap(), r3.clone())).unwrap(); - let key = TupleKey { subject: subject.clone(), relation: Relation::new("viewer").unwrap(), object: r2.clone() }; + engine + .write(&RelationshipTuple::new( + subject.clone(), + Relation::new("owner").unwrap(), + r1.clone(), + )) + .unwrap(); + engine + .write(&RelationshipTuple::new( + subject.clone(), + Relation::new("viewer").unwrap(), + r2.clone(), + )) + .unwrap(); + engine + .write(&RelationshipTuple::new( + subject.clone(), + Relation::new("owner").unwrap(), + r3.clone(), + )) + .unwrap(); + let key = TupleKey { + subject: subject.clone(), + relation: Relation::new("viewer").unwrap(), + object: r2.clone(), + }; engine.delete(&key).unwrap(); for r in &[&r1, &r3] { - let tuples = engine.storage().list_by_object(&PartitionId::default(), r, None, &ConsistencyMode::MinimizeLatency).unwrap(); + let tuples = engine + .storage() + .list_by_object( + &PartitionId::default(), + r, + None, + &ConsistencyMode::MinimizeLatency, + ) + .unwrap(); assert!(!tuples.is_empty(), "tuple for {:?} should still exist", r); } - let deleted_tuples = engine.storage().list_by_object(&PartitionId::default(), &r2, None, &ConsistencyMode::MinimizeLatency).unwrap(); + let deleted_tuples = engine + .storage() + .list_by_object( + &PartitionId::default(), + &r2, + None, + &ConsistencyMode::MinimizeLatency, + ) + .unwrap(); assert!(deleted_tuples.is_empty(), "deleted tuple should not exist"); } @@ -2420,14 +2749,16 @@ types: let sub = engine.watch(WatchFilter::default()); let subject = SubjectId::new("user:watch").unwrap(); for i in 0..3 { - engine.write(&RelationshipTuple::new( - subject.clone(), - Relation::new("owner").unwrap(), - ResourceId::new(&format!("repo:watch{i}")).unwrap(), - )).unwrap(); + engine + .write(&RelationshipTuple::new( + subject.clone(), + Relation::new("owner").unwrap(), + ResourceId::new(format!("repo:watch{i}")).unwrap(), + )) + .unwrap(); } let mut count = 0; - while let Ok(_) = sub.try_recv() { + while sub.try_recv().is_ok() { count += 1; } assert_eq!(count, 3); @@ -2440,12 +2771,24 @@ types: let engine = make_engine(); let subject = SubjectId::new("user:audit").unwrap(); let resource = ResourceId::new("repo:audit").unwrap(); - let token = engine.write(&RelationshipTuple::new( - subject.clone(), - Relation::new("owner").unwrap(), - resource.clone(), - )).unwrap(); - let entries = engine.query_audit(&resource, None, None, &PaginationParams { limit: 10, cursor: None }).unwrap(); + let token = engine + .write(&RelationshipTuple::new( + subject.clone(), + Relation::new("owner").unwrap(), + resource.clone(), + )) + .unwrap(); + let entries = engine + .query_audit( + &resource, + None, + None, + &PaginationParams { + limit: 10, + cursor: None, + }, + ) + .unwrap(); assert_eq!(entries.len(), 1); assert_eq!(entries[0].revision, token.revision); assert_eq!(entries[0].subject, "user:audit"); @@ -2461,17 +2804,29 @@ types: // Set actor identity before write engine.set_actor(Some("service-user")); - let token = engine.write(&RelationshipTuple::new( - subject.clone(), - Relation::new("owner").unwrap(), - resource.clone(), - )).unwrap(); + let token = engine + .write(&RelationshipTuple::new( + subject.clone(), + Relation::new("owner").unwrap(), + resource.clone(), + )) + .unwrap(); // Verify active_actor returns the identity assert_eq!(engine.active_actor(), Some("service-user".to_string())); // Verify identity appears in audit - let entries = engine.query_audit(&resource, None, None, &PaginationParams { limit: 10, cursor: None }).unwrap(); + let entries = engine + .query_audit( + &resource, + None, + None, + &PaginationParams { + limit: 10, + cursor: None, + }, + ) + .unwrap(); assert_eq!(entries.len(), 1); assert_eq!(entries[0].identity, Some("service-user".to_string())); assert_eq!(entries[0].revision, token.revision); @@ -2479,13 +2834,25 @@ types: // Clear actor and write again engine.set_actor(None); let subject2 = SubjectId::new("user:no_actor").unwrap(); - let _ = engine.write(&RelationshipTuple::new( - subject2.clone(), - Relation::new("viewer").unwrap(), - resource.clone(), - )).unwrap(); + let _ = engine + .write(&RelationshipTuple::new( + subject2.clone(), + Relation::new("viewer").unwrap(), + resource.clone(), + )) + .unwrap(); - let entries2 = engine.query_audit(&resource, None, None, &PaginationParams { limit: 10, cursor: None }).unwrap(); + let entries2 = engine + .query_audit( + &resource, + None, + None, + &PaginationParams { + limit: 10, + cursor: None, + }, + ) + .unwrap(); assert_eq!(entries2.len(), 2); // Second entry should have None identity assert_eq!(entries2[1].identity, None); @@ -2497,11 +2864,13 @@ types: let subject = SubjectId::new("user:del_actor").unwrap(); let resource = ResourceId::new("repo:del_actor").unwrap(); - engine.write(&RelationshipTuple::new( - subject.clone(), - Relation::new("owner").unwrap(), - resource.clone(), - )).unwrap(); + engine + .write(&RelationshipTuple::new( + subject.clone(), + Relation::new("owner").unwrap(), + resource.clone(), + )) + .unwrap(); // Set identity and delete engine.set_actor(Some("cleanup-service")); @@ -2512,7 +2881,17 @@ types: }; let _ = engine.delete(&key).unwrap(); - let entries = engine.query_audit(&resource, None, None, &PaginationParams { limit: 10, cursor: None }).unwrap(); + let entries = engine + .query_audit( + &resource, + None, + None, + &PaginationParams { + limit: 10, + cursor: None, + }, + ) + .unwrap(); assert_eq!(entries.len(), 2); // Write entry (no identity) then delete entry (with identity) assert_eq!(entries[0].identity, None); @@ -2527,14 +2906,28 @@ types: engine.set_actor(Some("txn-actor")); let mut txn = engine.transaction().unwrap(); - txn.write(&PartitionId::default(), &RelationshipTuple::new( - SubjectId::new("user:txn1").unwrap(), - Relation::new("owner").unwrap(), - resource.clone(), - )).unwrap(); + txn.write( + &PartitionId::default(), + &RelationshipTuple::new( + SubjectId::new("user:txn1").unwrap(), + Relation::new("owner").unwrap(), + resource.clone(), + ), + ) + .unwrap(); txn.commit().unwrap(); - let entries = engine.query_audit(&resource, None, None, &PaginationParams { limit: 10, cursor: None }).unwrap(); + let entries = engine + .query_audit( + &resource, + None, + None, + &PaginationParams { + limit: 10, + cursor: None, + }, + ) + .unwrap(); assert_eq!(entries.len(), 1); assert_eq!(entries[0].identity, Some("txn-actor".to_string())); } @@ -2552,7 +2945,10 @@ types: #[test] fn test_double_initialize() { - let config = SqliteConfig { path: ":memory:".to_string(), ..Default::default() }; + let config = SqliteConfig { + path: ":memory:".to_string(), + ..Default::default() + }; let mut storage = SqliteStorage::new(config).unwrap(); storage.initialize().unwrap(); storage.initialize().unwrap(); @@ -2566,7 +2962,13 @@ types: let engine = Arc::new(make_engine()); let subject = SubjectId::new("user:concurrent").unwrap(); let resource = ResourceId::new("repo:concurrent").unwrap(); - engine.write(&RelationshipTuple::new(subject.clone(), Relation::new("owner").unwrap(), resource.clone())).unwrap(); + engine + .write(&RelationshipTuple::new( + subject.clone(), + Relation::new("owner").unwrap(), + resource.clone(), + )) + .unwrap(); let mut handles = vec![]; for _ in 0..10 { let engine = Arc::clone(&engine); @@ -2593,9 +2995,13 @@ types: let lock = Arc::clone(&write_lock); handles.push(std::thread::spawn(move || { let _guard = lock.lock().unwrap(); - let subject = SubjectId::new(&format!("user:writer{i}")).unwrap(); - let resource = ResourceId::new(&format!("repo:writer{i}")).unwrap(); - engine.write(&RelationshipTuple::new(subject, Relation::new("owner").unwrap(), resource)) + let subject = SubjectId::new(format!("user:writer{i}")).unwrap(); + let resource = ResourceId::new(format!("repo:writer{i}")).unwrap(); + engine.write(&RelationshipTuple::new( + subject, + Relation::new("owner").unwrap(), + resource, + )) })); } for h in handles { @@ -2612,16 +3018,20 @@ types: for i in 0..5 { let engine = Arc::clone(&engine); writer_handles.push(std::thread::spawn(move || { - let subject = SubjectId::new(&format!("user:rw{i}")).unwrap(); - let resource = ResourceId::new(&format!("repo:rw{i}")).unwrap(); - engine.write(&RelationshipTuple::new(subject, Relation::new("owner").unwrap(), resource)) + let subject = SubjectId::new(format!("user:rw{i}")).unwrap(); + let resource = ResourceId::new(format!("repo:rw{i}")).unwrap(); + engine.write(&RelationshipTuple::new( + subject, + Relation::new("owner").unwrap(), + resource, + )) })); } let mut reader_handles = vec![]; for i in 0..10 { let engine = Arc::clone(&engine); reader_handles.push(std::thread::spawn(move || { - let subject = SubjectId::new(&format!("user:reader{i}")).unwrap(); + let subject = SubjectId::new(format!("user:reader{i}")).unwrap(); let resource = ResourceId::new("repo:any").unwrap(); let _ = engine.check(&subject, "read", &resource, None); })); @@ -2640,7 +3050,13 @@ types: let engine = Arc::new(make_engine()); let subject = SubjectId::new("user:pool").unwrap(); let resource = ResourceId::new("repo:pool").unwrap(); - engine.write(&RelationshipTuple::new(subject.clone(), Relation::new("owner").unwrap(), resource.clone())).unwrap(); + engine + .write(&RelationshipTuple::new( + subject.clone(), + Relation::new("owner").unwrap(), + resource.clone(), + )) + .unwrap(); let mut handles = vec![]; for _ in 0..20 { let engine = Arc::clone(&engine); @@ -2661,12 +3077,24 @@ types: let engine = make_engine(); let root = SubjectId::new("user:deep").unwrap(); let mut prev = ResourceId::new("repo:level0").unwrap(); - engine.write(&RelationshipTuple::new(root.clone(), Relation::new("owner").unwrap(), prev.clone())).unwrap(); + engine + .write(&RelationshipTuple::new( + root.clone(), + Relation::new("owner").unwrap(), + prev.clone(), + )) + .unwrap(); let depth = 5; for i in 1..depth { - let current = ResourceId::new(&format!("repo:level{i}")).unwrap(); - let as_subject = SubjectId::new(&format!("repo:level{}", i - 1)).unwrap(); - engine.write(&RelationshipTuple::new(as_subject, Relation::new("owner").unwrap(), current.clone())).unwrap(); + let current = ResourceId::new(format!("repo:level{i}")).unwrap(); + let as_subject = SubjectId::new(format!("repo:level{}", i - 1)).unwrap(); + engine + .write(&RelationshipTuple::new( + as_subject, + Relation::new("owner").unwrap(), + current.clone(), + )) + .unwrap(); prev = current; } let result = engine.check(&root, "read", &prev, None).unwrap(); @@ -2678,15 +3106,23 @@ types: let engine = make_engine(); let resource = ResourceId::new("repo:siblings").unwrap(); for i in 0..100 { - let subject = SubjectId::new(&format!("user:sib{i}")).unwrap(); - engine.write(&RelationshipTuple::new(subject, Relation::new("owner").unwrap(), resource.clone())).unwrap(); + let subject = SubjectId::new(format!("user:sib{i}")).unwrap(); + engine + .write(&RelationshipTuple::new( + subject, + Relation::new("owner").unwrap(), + resource.clone(), + )) + .unwrap(); } - let result = engine.check( - &SubjectId::new("user:sib0").unwrap(), - "read", - &resource, - None, - ).unwrap(); + let result = engine + .check( + &SubjectId::new("user:sib0").unwrap(), + "read", + &resource, + None, + ) + .unwrap(); assert!(result.allowed); } @@ -2697,10 +3133,27 @@ types: let engine = make_engine(); let subject = SubjectId::new("user:persist").unwrap(); let resource = ResourceId::new("repo:persist").unwrap(); - engine.write(&RelationshipTuple::new(subject.clone(), Relation::new("owner").unwrap(), resource.clone())).unwrap(); - let _rev1 = engine.storage().current_revision(&PartitionId::default()).unwrap(); + engine + .write(&RelationshipTuple::new( + subject.clone(), + Relation::new("owner").unwrap(), + resource.clone(), + )) + .unwrap(); + let _rev1 = engine + .storage() + .current_revision(&PartitionId::default()) + .unwrap(); engine.recover_from_events(None).unwrap(); - let tuples = engine.storage().list_by_object(&PartitionId::default(), &resource, None, &ConsistencyMode::MinimizeLatency).unwrap(); + let tuples = engine + .storage() + .list_by_object( + &PartitionId::default(), + &resource, + None, + &ConsistencyMode::MinimizeLatency, + ) + .unwrap(); assert_eq!(tuples.len(), 1); } @@ -2711,14 +3164,25 @@ types: let engine = make_engine(); let subject = SubjectId::new("user:page").unwrap(); for i in 0..100 { - let resource = ResourceId::new(&format!("repo:page{i}")).unwrap(); - engine.write(&RelationshipTuple::new(subject.clone(), Relation::new("owner").unwrap(), resource)).unwrap(); + let resource = ResourceId::new(format!("repo:page{i}")).unwrap(); + engine + .write(&RelationshipTuple::new( + subject.clone(), + Relation::new("owner").unwrap(), + resource, + )) + .unwrap(); } - let result = engine.query( - &TupleFilter::default(), - &PaginationParams { limit: 10, cursor: None }, - None, - ).unwrap(); + let result = engine + .query( + &TupleFilter::default(), + &PaginationParams { + limit: 10, + cursor: None, + }, + None, + ) + .unwrap(); assert_eq!(result.tuples.len(), 10); assert!(result.next_cursor.is_some()); } @@ -2734,15 +3198,57 @@ types: types: { let mut types = std::collections::HashMap::new(); let mut repo_relations = std::collections::HashMap::new(); - repo_relations.insert("owner".to_string(), RelationDef { inherit_from: vec![], description: None }); + repo_relations.insert( + "owner".to_string(), + RelationDef { + inherit_from: vec![], + description: None, + }, + ); let mut repo_permissions = std::collections::HashMap::new(); - repo_permissions.insert("read".to_string(), PermissionDef { union_of: vec!["owner".to_string()], condition: None, description: None, ..Default::default() }); - types.insert("repo".to_string(), TypeDef { relations: repo_relations, permissions: repo_permissions, ..Default::default() }); + repo_permissions.insert( + "read".to_string(), + PermissionDef { + union_of: vec!["owner".to_string()], + condition: None, + description: None, + ..Default::default() + }, + ); + types.insert( + "repo".to_string(), + TypeDef { + relations: repo_relations, + permissions: repo_permissions, + ..Default::default() + }, + ); let mut doc_relations = std::collections::HashMap::new(); - doc_relations.insert("editor".to_string(), RelationDef { inherit_from: vec![], description: None }); + doc_relations.insert( + "editor".to_string(), + RelationDef { + inherit_from: vec![], + description: None, + }, + ); let mut doc_permissions = std::collections::HashMap::new(); - doc_permissions.insert("read".to_string(), PermissionDef { union_of: vec!["editor".to_string()], condition: None, description: None, ..Default::default() }); - types.insert("doc".to_string(), TypeDef { relations: doc_relations, permissions: doc_permissions, ..Default::default() }); + doc_permissions.insert( + "read".to_string(), + PermissionDef { + union_of: vec!["editor".to_string()], + condition: None, + description: None, + ..Default::default() + }, + ); + types.insert( + "doc".to_string(), + TypeDef { + relations: doc_relations, + permissions: doc_permissions, + ..Default::default() + }, + ); types }, }; @@ -2752,12 +3258,29 @@ types: let alice = SubjectId::new("user:alice").unwrap(); let repo = ResourceId::new("repo:test").unwrap(); let doc = ResourceId::new("doc:test").unwrap(); - engine.write(&RelationshipTuple::new(alice.clone(), Relation::new("owner").unwrap(), repo.clone())).unwrap(); - engine.write(&RelationshipTuple::new(alice.clone(), Relation::new("editor").unwrap(), doc.clone())).unwrap(); + engine + .write(&RelationshipTuple::new( + alice.clone(), + Relation::new("owner").unwrap(), + repo.clone(), + )) + .unwrap(); + engine + .write(&RelationshipTuple::new( + alice.clone(), + Relation::new("editor").unwrap(), + doc.clone(), + )) + .unwrap(); assert!(engine.check(&alice, "read", &repo, None).unwrap().allowed); assert!(engine.check(&alice, "read", &doc, None).unwrap().allowed); - let filter = TupleFilter { object_type: Some("repo".to_string()), ..Default::default() }; - let result = engine.query(&filter, &PaginationParams::default(), None).unwrap(); + let filter = TupleFilter { + object_type: Some("repo".to_string()), + ..Default::default() + }; + let result = engine + .query(&filter, &PaginationParams::default(), None) + .unwrap(); assert_eq!(result.tuples.len(), 1); assert!(result.tuples[0].object.as_str().starts_with("repo:")); } @@ -2771,15 +3294,31 @@ types: types: { let mut types = std::collections::HashMap::new(); let mut relations = std::collections::HashMap::new(); - relations.insert("viewer".to_string(), RelationDef { inherit_from: vec![], description: None }); + relations.insert( + "viewer".to_string(), + RelationDef { + inherit_from: vec![], + description: None, + }, + ); let mut permissions = std::collections::HashMap::new(); - permissions.insert("read".to_string(), PermissionDef { - union_of: vec!["viewer".to_string()], - condition: Some("role eq admin".to_string()), - description: None, - ..Default::default() - }); - types.insert("repo".to_string(), TypeDef { relations, permissions, ..Default::default() }); + permissions.insert( + "read".to_string(), + PermissionDef { + union_of: vec!["viewer".to_string()], + condition: Some("role eq admin".to_string()), + description: None, + ..Default::default() + }, + ); + types.insert( + "repo".to_string(), + TypeDef { + relations, + permissions, + ..Default::default() + }, + ); types }, }; @@ -2788,7 +3327,13 @@ types: let engine = GraphEngine::new(Box::new(storage), schema); let alice = SubjectId::new("user:alice").unwrap(); let repo = ResourceId::new("repo:test").unwrap(); - engine.write(&RelationshipTuple::new(alice.clone(), Relation::new("viewer").unwrap(), repo.clone())).unwrap(); + engine + .write(&RelationshipTuple::new( + alice.clone(), + Relation::new("viewer").unwrap(), + repo.clone(), + )) + .unwrap(); // Without context — condition present but no metadata → denied let result = engine.check(&alice, "read", &repo, None).unwrap(); @@ -2796,14 +3341,20 @@ types: // With matching context — role eq admin let mut ctx = crate::engine::condition::ConditionEvalContext::default(); - ctx.subject_meta.insert("role".to_string(), "admin".to_string()); - let result = engine.check_with_context(&alice, "read", &repo, None, ctx).unwrap(); + ctx.subject_meta + .insert("role".to_string(), "admin".to_string()); + let result = engine + .check_with_context(&alice, "read", &repo, None, ctx) + .unwrap(); assert!(result.allowed, "matching context should allow"); // With non-matching context — role eq viewer let mut ctx = crate::engine::condition::ConditionEvalContext::default(); - ctx.subject_meta.insert("role".to_string(), "viewer".to_string()); - let result = engine.check_with_context(&alice, "read", &repo, None, ctx).unwrap(); + ctx.subject_meta + .insert("role".to_string(), "viewer".to_string()); + let result = engine + .check_with_context(&alice, "read", &repo, None, ctx) + .unwrap(); assert!(!result.allowed, "non-matching context should deny"); } @@ -2816,15 +3367,31 @@ types: types: { let mut types = std::collections::HashMap::new(); let mut relations = std::collections::HashMap::new(); - relations.insert("viewer".to_string(), RelationDef { inherit_from: vec![], description: None }); + relations.insert( + "viewer".to_string(), + RelationDef { + inherit_from: vec![], + description: None, + }, + ); let mut permissions = std::collections::HashMap::new(); - permissions.insert("read".to_string(), PermissionDef { - union_of: vec!["viewer".to_string()], - condition: Some("role eq admin".to_string()), - description: None, - ..Default::default() - }); - types.insert("repo".to_string(), TypeDef { relations, permissions, ..Default::default() }); + permissions.insert( + "read".to_string(), + PermissionDef { + union_of: vec!["viewer".to_string()], + condition: Some("role eq admin".to_string()), + description: None, + ..Default::default() + }, + ); + types.insert( + "repo".to_string(), + TypeDef { + relations, + permissions, + ..Default::default() + }, + ); types }, }; @@ -2833,11 +3400,20 @@ types: let engine = GraphEngine::new(Box::new(storage), schema); let alice = SubjectId::new("user:alice").unwrap(); let repo = ResourceId::new("repo:test").unwrap(); - engine.write(&RelationshipTuple::new(alice.clone(), Relation::new("viewer").unwrap(), repo.clone())).unwrap(); + engine + .write(&RelationshipTuple::new( + alice.clone(), + Relation::new("viewer").unwrap(), + repo.clone(), + )) + .unwrap(); let mut ctx = crate::engine::condition::ConditionEvalContext::default(); - ctx.subject_meta.insert("role".to_string(), "admin".to_string()); - let result = engine.check_dry_run_with_context(&alice, "read", &repo, None, ctx).unwrap(); + ctx.subject_meta + .insert("role".to_string(), "admin".to_string()); + let result = engine + .check_dry_run_with_context(&alice, "read", &repo, None, ctx) + .unwrap(); assert!(result.allowed, "dry-run with matching context should allow"); // dry_run without context @@ -2854,20 +3430,43 @@ types: types: { let mut types = std::collections::HashMap::new(); let mut relations = std::collections::HashMap::new(); - relations.insert("owner".to_string(), RelationDef { inherit_from: vec![], description: None }); - relations.insert("banned".to_string(), RelationDef { inherit_from: vec![], description: None }); + relations.insert( + "owner".to_string(), + RelationDef { + inherit_from: vec![], + description: None, + }, + ); + relations.insert( + "banned".to_string(), + RelationDef { + inherit_from: vec![], + description: None, + }, + ); let mut permissions = std::collections::HashMap::new(); - permissions.insert("read".to_string(), PermissionDef { - union_of: vec!["owner".to_string()], - condition: None, - description: None, - ..Default::default() - }); + permissions.insert( + "read".to_string(), + PermissionDef { + union_of: vec!["owner".to_string()], + condition: None, + description: None, + ..Default::default() + }, + ); let deny = vec![DenyDef { relations: vec!["banned".to_string()], description: Some("banned users cannot read".to_string()), }]; - types.insert("repo".to_string(), TypeDef { relations, permissions, deny, ..Default::default() }); + types.insert( + "repo".to_string(), + TypeDef { + relations, + permissions, + deny, + ..Default::default() + }, + ); types }, }; @@ -2878,14 +3477,29 @@ types: let repo = ResourceId::new("repo:test").unwrap(); // Alice is owner — should be allowed - engine.write(&RelationshipTuple::new(alice.clone(), Relation::new("owner").unwrap(), repo.clone())).unwrap(); + engine + .write(&RelationshipTuple::new( + alice.clone(), + Relation::new("owner").unwrap(), + repo.clone(), + )) + .unwrap(); let result = engine.check(&alice, "read", &repo, None).unwrap(); assert!(result.allowed, "owner should be allowed to read"); // Alice is also banned — deny should override allow - engine.write(&RelationshipTuple::new(alice.clone(), Relation::new("banned").unwrap(), repo.clone())).unwrap(); + engine + .write(&RelationshipTuple::new( + alice.clone(), + Relation::new("banned").unwrap(), + repo.clone(), + )) + .unwrap(); let result = engine.check(&alice, "read", &repo, None).unwrap(); - assert!(!result.allowed, "deny rule for banned should override owner allow"); + assert!( + !result.allowed, + "deny rule for banned should override owner allow" + ); } #[test] @@ -2897,15 +3511,31 @@ types: types: { let mut types = std::collections::HashMap::new(); let mut relations = std::collections::HashMap::new(); - relations.insert("admin".to_string(), RelationDef { inherit_from: vec![], description: None }); + relations.insert( + "admin".to_string(), + RelationDef { + inherit_from: vec![], + description: None, + }, + ); let mut permissions = std::collections::HashMap::new(); - permissions.insert("admin".to_string(), PermissionDef { - union_of: vec!["admin".to_string()], - condition: None, - description: None, - ..Default::default() - }); - types.insert("repo".to_string(), TypeDef { relations, permissions, ..Default::default() }); + permissions.insert( + "admin".to_string(), + PermissionDef { + union_of: vec!["admin".to_string()], + condition: None, + description: None, + ..Default::default() + }, + ); + types.insert( + "repo".to_string(), + TypeDef { + relations, + permissions, + ..Default::default() + }, + ); types }, }; @@ -2920,7 +3550,8 @@ types: assert!(token.revision.as_u64() > 0); // Check role - let result = rbac::check_role(&engine, &PartitionId::default(), &alice, "admin", &repo).unwrap(); + let result = + rbac::check_role(&engine, &PartitionId::default(), &alice, "admin", &repo).unwrap(); assert!(result.allowed, "alice should have admin role"); // Get roles @@ -2930,7 +3561,8 @@ types: // Unassign role let _ = rbac::unassign_role(&engine, &alice, "admin", &repo).unwrap(); - let result = rbac::check_role(&engine, &PartitionId::default(), &alice, "admin", &repo).unwrap(); + let result = + rbac::check_role(&engine, &PartitionId::default(), &alice, "admin", &repo).unwrap(); assert!(!result.allowed, "alice should no longer have admin role"); } @@ -2943,22 +3575,47 @@ types: types: { let mut types = std::collections::HashMap::new(); let mut relations = std::collections::HashMap::new(); - relations.insert("viewer".to_string(), RelationDef { inherit_from: vec![], description: None }); - relations.insert("editor".to_string(), RelationDef { inherit_from: vec![], description: None }); + relations.insert( + "viewer".to_string(), + RelationDef { + inherit_from: vec![], + description: None, + }, + ); + relations.insert( + "editor".to_string(), + RelationDef { + inherit_from: vec![], + description: None, + }, + ); let mut permissions = std::collections::HashMap::new(); - permissions.insert("read".to_string(), PermissionDef { - union_of: vec!["viewer".to_string(), "editor".to_string()], - condition: None, - description: None, - ..Default::default() - }); - permissions.insert("write".to_string(), PermissionDef { - union_of: vec!["editor".to_string()], - condition: None, - description: None, - ..Default::default() - }); - types.insert("repo".to_string(), TypeDef { relations, permissions, ..Default::default() }); + permissions.insert( + "read".to_string(), + PermissionDef { + union_of: vec!["viewer".to_string(), "editor".to_string()], + condition: None, + description: None, + ..Default::default() + }, + ); + permissions.insert( + "write".to_string(), + PermissionDef { + union_of: vec!["editor".to_string()], + condition: None, + description: None, + ..Default::default() + }, + ); + types.insert( + "repo".to_string(), + TypeDef { + relations, + permissions, + ..Default::default() + }, + ); types }, }; @@ -3001,15 +3658,31 @@ types: types: { let mut types = std::collections::HashMap::new(); let mut relations = std::collections::HashMap::new(); - relations.insert("member".to_string(), RelationDef { inherit_from: vec![], description: None }); + relations.insert( + "member".to_string(), + RelationDef { + inherit_from: vec![], + description: None, + }, + ); let mut permissions = std::collections::HashMap::new(); - permissions.insert("member".to_string(), PermissionDef { - union_of: vec!["member".to_string()], - condition: None, - description: None, - ..Default::default() - }); - types.insert("team".to_string(), TypeDef { relations, permissions, ..Default::default() }); + permissions.insert( + "member".to_string(), + PermissionDef { + union_of: vec!["member".to_string()], + condition: None, + description: None, + ..Default::default() + }, + ); + types.insert( + "team".to_string(), + TypeDef { + relations, + permissions, + ..Default::default() + }, + ); types }, }; @@ -3043,9 +3716,18 @@ types: let logs = logged.lock().unwrap(); let joined = logs.join(" "); - assert!(!joined.contains("secret"), "Logs should not contain secrets: {joined}"); - assert!(!joined.contains("password"), "Logs should not contain passwords: {joined}"); - assert!(!joined.contains("api_key"), "Logs should not contain api_key: {joined}"); + assert!( + !joined.contains("secret"), + "Logs should not contain secrets: {joined}" + ); + assert!( + !joined.contains("password"), + "Logs should not contain passwords: {joined}" + ); + assert!( + !joined.contains("api_key"), + "Logs should not contain api_key: {joined}" + ); } #[test] @@ -3057,20 +3739,43 @@ types: types: { let mut types = std::collections::HashMap::new(); let mut relations = std::collections::HashMap::new(); - relations.insert("member".to_string(), RelationDef { inherit_from: vec![], description: None }); - relations.insert("banned".to_string(), RelationDef { inherit_from: vec![], description: None }); + relations.insert( + "member".to_string(), + RelationDef { + inherit_from: vec![], + description: None, + }, + ); + relations.insert( + "banned".to_string(), + RelationDef { + inherit_from: vec![], + description: None, + }, + ); let mut permissions = std::collections::HashMap::new(); - permissions.insert("access".to_string(), PermissionDef { - union_of: vec!["member".to_string()], - condition: None, - description: None, - ..Default::default() - }); + permissions.insert( + "access".to_string(), + PermissionDef { + union_of: vec!["member".to_string()], + condition: None, + description: None, + ..Default::default() + }, + ); let deny = vec![DenyDef { relations: vec!["banned".to_string()], description: Some("banned users denied".to_string()), }]; - types.insert("workspace".to_string(), TypeDef { relations, permissions, deny, ..Default::default() }); + types.insert( + "workspace".to_string(), + TypeDef { + relations, + permissions, + deny, + ..Default::default() + }, + ); types }, }; @@ -3081,7 +3786,13 @@ types: let ws = ResourceId::new("workspace:acme").unwrap(); // Alice is a member (no deny relation) - engine.write(&RelationshipTuple::new(alice.clone(), Relation::new("member").unwrap(), ws.clone())).unwrap(); + engine + .write(&RelationshipTuple::new( + alice.clone(), + Relation::new("member").unwrap(), + ws.clone(), + )) + .unwrap(); let result = engine.check(&alice, "access", &ws, None).unwrap(); assert!(result.allowed, "member without ban should be allowed"); } diff --git a/crates/aegis-core/src/engine/partition.rs b/crates/aegis-core/src/engine/partition.rs index d2a9778..9707248 100644 --- a/crates/aegis-core/src/engine/partition.rs +++ b/crates/aegis-core/src/engine/partition.rs @@ -1,7 +1,7 @@ //! Partition management for isolated authorization graphs. -use crate::error::AegisResult; use crate::engine::ratelimit::{RateLimitConfig, RateLimitOp, TokenBucketRateLimiter}; +use crate::error::AegisResult; use crate::types::PartitionId; use std::collections::HashMap; use std::sync::Mutex; @@ -16,6 +16,12 @@ struct PartitionState { rate_limiter: TokenBucketRateLimiter, } +impl Default for PartitionManager { + fn default() -> Self { + Self::new() + } +} + impl PartitionManager { pub fn new() -> Self { Self { @@ -28,26 +34,35 @@ impl PartitionManager { pub fn get_or_create(&self, partition_id: &PartitionId) -> AegisResult { let key = partition_id.to_string(); - let mut map = self.partitions.lock().map_err(|_| { - crate::error::AegisError::Internal("partition lock poisoned".into()) - })?; + let mut map = self + .partitions + .lock() + .map_err(|_| crate::error::AegisError::Internal("partition lock poisoned".into()))?; if !map.contains_key(&key) { - map.insert(key.clone(), PartitionState { - rate_limiter: TokenBucketRateLimiter::new(RateLimitConfig::default()), - }); + map.insert( + key.clone(), + PartitionState { + rate_limiter: TokenBucketRateLimiter::new(RateLimitConfig::default()), + }, + ); } - Ok(PartitionHandle { partition_id: partition_id.clone() }) + Ok(PartitionHandle { + partition_id: partition_id.clone(), + }) } pub fn check_rate_limit(&self, partition_id: &PartitionId) -> AegisResult<()> { let key = partition_id.to_string(); + #[allow(clippy::collapsible_if)] if let Ok(map) = self.partitions.lock() { if let Some(state) = map.get(&key) { return state.rate_limiter.check(&key, RateLimitOp::Check); } } // If no partition-specific state, use default - self.default_partition.rate_limiter.check(&key, RateLimitOp::Check) + self.default_partition + .rate_limiter + .check(&key, RateLimitOp::Check) } } diff --git a/crates/aegis-core/src/engine/policy.rs b/crates/aegis-core/src/engine/policy.rs index 770032c..6e26812 100644 --- a/crates/aegis-core/src/engine/policy.rs +++ b/crates/aegis-core/src/engine/policy.rs @@ -82,7 +82,11 @@ mod tests { repo_perms.insert( "read".to_string(), PermissionDef { - union_of: vec!["viewer".to_string(), "editor".to_string(), "owner".to_string()], + union_of: vec![ + "viewer".to_string(), + "editor".to_string(), + "owner".to_string(), + ], condition: None, description: None, ..Default::default() diff --git a/crates/aegis-core/src/engine/policy_lifecycle.rs b/crates/aegis-core/src/engine/policy_lifecycle.rs index 7dbe7e7..4ea6526 100644 --- a/crates/aegis-core/src/engine/policy_lifecycle.rs +++ b/crates/aegis-core/src/engine/policy_lifecycle.rs @@ -3,8 +3,8 @@ use uuid::Uuid; use crate::engine::GraphEngine; use crate::error::{AegisError, AegisResult}; -use crate::types::analysis::{AccessDiffReport, SimulationReport}; use crate::types::Schema; +use crate::types::analysis::{AccessDiffReport, SimulationReport}; /// Status of a policy draft in its lifecycle. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] @@ -67,14 +67,13 @@ pub struct PublishResult { impl GraphEngine { /// Create a new policy draft in Drafting status. - pub fn create_policy_draft( - &self, - name: &str, - description: &str, - ) -> AegisResult { + pub fn create_policy_draft(&self, name: &str, description: &str) -> AegisResult { let now = chrono::Utc::now().to_rfc3339(); let schema = { - let s = self.schema.read().map_err(|e| AegisError::Internal(e.to_string()))?; + let s = self + .schema + .read() + .map_err(|e| AegisError::Internal(e.to_string()))?; s.clone() }; let current_ver = self.storage.read_schema_version().unwrap_or(0); @@ -95,7 +94,10 @@ impl GraphEngine { }; { - let mut drafts = self.drafts.lock().map_err(|e| AegisError::Internal(e.to_string()))?; + let mut drafts = self + .drafts + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; drafts.insert(draft.id, draft.clone()); } @@ -106,14 +108,18 @@ impl GraphEngine { /// Update a draft's schema (only allowed in Drafting status). pub fn update_policy_draft(&self, id: Uuid, schema: Schema) -> AegisResult { - let mut drafts = self.drafts.lock().map_err(|e| AegisError::Internal(e.to_string()))?; - let draft = drafts.get_mut(&id).ok_or_else(|| { - AegisError::Internal(format!("draft {} not found", id)) - })?; + let mut drafts = self + .drafts + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; + let draft = drafts + .get_mut(&id) + .ok_or_else(|| AegisError::Internal(format!("draft {} not found", id)))?; if draft.status != DraftStatus::Drafting { return Err(AegisError::Internal(format!( - "cannot update draft in status {:?}", draft.status + "cannot update draft in status {:?}", + draft.status ))); } @@ -126,17 +132,24 @@ impl GraphEngine { /// Validate a draft: check schema validity, compute diff, and run simulation. pub fn validate_policy_draft(&self, id: Uuid) -> AegisResult { let draft = { - let drafts = self.drafts.lock().map_err(|e| AegisError::Internal(e.to_string()))?; - drafts.get(&id).cloned().ok_or_else(|| { - AegisError::Internal(format!("draft {} not found", id)) - })? + let drafts = self + .drafts + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; + drafts + .get(&id) + .cloned() + .ok_or_else(|| AegisError::Internal(format!("draft {} not found", id)))? }; let mut warnings = Vec::new(); let schema_valid = true; let current_schema = { - let s = self.schema.read().map_err(|e| AegisError::Internal(e.to_string()))?; + let s = self + .schema + .read() + .map_err(|e| AegisError::Internal(e.to_string()))?; s.clone() }; @@ -158,14 +171,18 @@ impl GraphEngine { /// Submit a draft for review. Must be in Drafting status. pub fn submit_policy_draft_for_review(&self, id: Uuid) -> AegisResult { - let mut drafts = self.drafts.lock().map_err(|e| AegisError::Internal(e.to_string()))?; - let draft = drafts.get_mut(&id).ok_or_else(|| { - AegisError::Internal(format!("draft {} not found", id)) - })?; + let mut drafts = self + .drafts + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; + let draft = drafts + .get_mut(&id) + .ok_or_else(|| AegisError::Internal(format!("draft {} not found", id)))?; if draft.status != DraftStatus::Drafting { return Err(AegisError::Internal(format!( - "cannot submit draft in status {:?}", draft.status + "cannot submit draft in status {:?}", + draft.status ))); } @@ -177,14 +194,18 @@ impl GraphEngine { /// Approve a draft. Must be UnderReview. pub fn approve_policy_draft(&self, id: Uuid) -> AegisResult { - let mut drafts = self.drafts.lock().map_err(|e| AegisError::Internal(e.to_string()))?; - let draft = drafts.get_mut(&id).ok_or_else(|| { - AegisError::Internal(format!("draft {} not found", id)) - })?; + let mut drafts = self + .drafts + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; + let draft = drafts + .get_mut(&id) + .ok_or_else(|| AegisError::Internal(format!("draft {} not found", id)))?; if draft.status != DraftStatus::UnderReview { return Err(AegisError::Internal(format!( - "cannot approve draft in status {:?}", draft.status + "cannot approve draft in status {:?}", + draft.status ))); } @@ -197,14 +218,18 @@ impl GraphEngine { /// Reject a draft. Must be UnderReview. pub fn reject_policy_draft(&self, id: Uuid, reason: &str) -> AegisResult { - let mut drafts = self.drafts.lock().map_err(|e| AegisError::Internal(e.to_string()))?; - let draft = drafts.get_mut(&id).ok_or_else(|| { - AegisError::Internal(format!("draft {} not found", id)) - })?; + let mut drafts = self + .drafts + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; + let draft = drafts + .get_mut(&id) + .ok_or_else(|| AegisError::Internal(format!("draft {} not found", id)))?; if draft.status != DraftStatus::UnderReview { return Err(AegisError::Internal(format!( - "cannot reject draft in status {:?}", draft.status + "cannot reject draft in status {:?}", + draft.status ))); } @@ -218,14 +243,18 @@ impl GraphEngine { /// Publish a draft: rolls the policy to the draft's schema. Draft must be Approved. pub fn publish_policy_draft(&self, id: Uuid) -> AegisResult { let draft = { - let mut drafts = self.drafts.lock().map_err(|e| AegisError::Internal(e.to_string()))?; - let draft = drafts.get_mut(&id).ok_or_else(|| { - AegisError::Internal(format!("draft {} not found", id)) - })?; + let mut drafts = self + .drafts + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; + let draft = drafts + .get_mut(&id) + .ok_or_else(|| AegisError::Internal(format!("draft {} not found", id)))?; if draft.status != DraftStatus::Approved { return Err(AegisError::Internal(format!( - "cannot publish draft in status {:?}", draft.status + "cannot publish draft in status {:?}", + draft.status ))); } draft.clone() @@ -233,11 +262,16 @@ impl GraphEngine { // Compute reports before publishing (capture current vs new state) let current_schema = { - let s = self.schema.read().map_err(|e| AegisError::Internal(e.to_string()))?; + let s = self + .schema + .read() + .map_err(|e| AegisError::Internal(e.to_string()))?; s.clone() }; - let access_diff = self.access_diff(¤t_schema, &draft.schema, None, Some(1000)).ok(); + let access_diff = self + .access_diff(¤t_schema, &draft.schema, None, Some(1000)) + .ok(); // Save the draft's schema as a new policy version via rollback mechanism let schema_json = serde_json::to_string(&draft.schema) @@ -254,14 +288,20 @@ impl GraphEngine { // Apply the draft schema { - let mut schema = self.schema.write().map_err(|e| AegisError::Internal(e.to_string()))?; + let mut schema = self + .schema + .write() + .map_err(|e| AegisError::Internal(e.to_string()))?; *schema = draft.schema; } self.storage.write_schema_version(current_ver + 1)?; // Update draft status { - let mut drafts = self.drafts.lock().map_err(|e| AegisError::Internal(e.to_string()))?; + let mut drafts = self + .drafts + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; if let Some(d) = drafts.get_mut(&id) { d.status = DraftStatus::Published; d.updated_at = chrono::Utc::now().to_rfc3339(); @@ -278,10 +318,13 @@ impl GraphEngine { /// Archive a draft (soft delete). pub fn archive_policy_draft(&self, id: Uuid) -> AegisResult { - let mut drafts = self.drafts.lock().map_err(|e| AegisError::Internal(e.to_string()))?; - let draft = drafts.get_mut(&id).ok_or_else(|| { - AegisError::Internal(format!("draft {} not found", id)) - })?; + let mut drafts = self + .drafts + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; + let draft = drafts + .get_mut(&id) + .ok_or_else(|| AegisError::Internal(format!("draft {} not found", id)))?; draft.status = DraftStatus::Archived; draft.updated_at = chrono::Utc::now().to_rfc3339(); @@ -290,8 +333,14 @@ impl GraphEngine { } /// List policy drafts, optionally filtered by status. - pub fn list_policy_drafts(&self, filter_status: Option) -> AegisResult> { - let drafts = self.drafts.lock().map_err(|e| AegisError::Internal(e.to_string()))?; + pub fn list_policy_drafts( + &self, + filter_status: Option, + ) -> AegisResult> { + let drafts = self + .drafts + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; let mut result: Vec = drafts.values().cloned().collect(); if let Some(status) = filter_status { result.retain(|d| d.status == status); diff --git a/crates/aegis-core/src/engine/ratelimit.rs b/crates/aegis-core/src/engine/ratelimit.rs index 4cf94bc..b4ebf18 100644 --- a/crates/aegis-core/src/engine/ratelimit.rs +++ b/crates/aegis-core/src/engine/ratelimit.rs @@ -68,12 +68,14 @@ impl TokenBucketRateLimiter { /// Check if an operation is allowed for the given key. /// Returns `RateLimitExceeded` error if the rate limit is exceeded. pub fn check(&self, key: &str, op: RateLimitOp) -> AegisResult<()> { - let mut buckets = self.buckets.lock().map_err(|e| { - AegisError::Internal(format!("rate limiter lock poisoned: {e}")) - })?; + let mut buckets = self + .buckets + .lock() + .map_err(|e| AegisError::Internal(format!("rate limiter lock poisoned: {e}")))?; // Evict the least-recently-accessed entry if we need to insert a new key // and the map is at capacity. + #[allow(clippy::collapsible_if)] if !buckets.contains_key(key) && buckets.len() >= self.config.max_keys { if let Some(oldest_key) = buckets .iter() @@ -97,8 +99,14 @@ impl TokenBucketRateLimiter { }); let (rate, burst) = match op { - RateLimitOp::Check => (self.config.checks_per_second as f64, self.config.check_burst as f64), - RateLimitOp::Write => (self.config.writes_per_second as f64, self.config.write_burst as f64), + RateLimitOp::Check => ( + self.config.checks_per_second as f64, + self.config.check_burst as f64, + ), + RateLimitOp::Write => ( + self.config.writes_per_second as f64, + self.config.write_burst as f64, + ), }; let now = Instant::now(); @@ -108,22 +116,36 @@ impl TokenBucketRateLimiter { state.last_accessed = now; if state.tokens < 1.0 { - let sanitized: String = key.chars().filter(|&c| c.is_alphanumeric() || c == ':' || c == '_' || c == '-').take(128).collect(); + let sanitized: String = key + .chars() + .filter(|&c| c.is_alphanumeric() || c == ':' || c == '_' || c == '-') + .take(128) + .collect(); tracing::warn!( "rate_limit.throttled key={} op={}", sanitized, - match op { RateLimitOp::Check => "check", RateLimitOp::Write => "write" }, + match op { + RateLimitOp::Check => "check", + RateLimitOp::Write => "write", + }, ); return Err(AegisError::RateLimitExceeded(key.to_string())); } state.tokens -= 1.0; - let sanitized: String = key.chars().filter(|&c| c.is_alphanumeric() || c == ':' || c == '_' || c == '-').take(128).collect(); + let sanitized: String = key + .chars() + .filter(|&c| c.is_alphanumeric() || c == ':' || c == '_' || c == '-') + .take(128) + .collect(); tracing::debug!( "rate_limit.allowed key={} op={} tokens_remaining={}", sanitized, - match op { RateLimitOp::Check => "check", RateLimitOp::Write => "write" }, + match op { + RateLimitOp::Check => "check", + RateLimitOp::Write => "write", + }, state.tokens, ); diff --git a/crates/aegis-core/src/engine/rbac.rs b/crates/aegis-core/src/engine/rbac.rs index 98b540a..4ffaeda 100644 --- a/crates/aegis-core/src/engine/rbac.rs +++ b/crates/aegis-core/src/engine/rbac.rs @@ -73,11 +73,7 @@ pub fn assign_role( role: &str, resource: &ResourceId, ) -> AegisResult { - let tuple = RelationshipTuple::new( - subject.clone(), - Relation::new(role)?, - resource.clone(), - ); + let tuple = RelationshipTuple::new(subject.clone(), Relation::new(role)?, resource.clone()); engine.write(&tuple) } @@ -122,9 +118,16 @@ pub fn check_role( // Check if subject has the child role relation directly. // Use list_by_subject to check for a direct tuple match, // since engine.check would resolve it as a permission (not what we want). - let tuples = engine.list_by_subject(subject, Some(&Relation::new(child_role_name).unwrap()), None)?; + let tuples = engine.list_by_subject( + subject, + Some(&Relation::new(child_role_name).unwrap()), + None, + )?; if tuples.iter().any(|t| t.object == *resource) { - let rev = engine.storage().current_revision(partition_id).unwrap_or(Revision::ZERO); + let rev = engine + .storage() + .current_revision(partition_id) + .unwrap_or(Revision::ZERO); return Ok(CheckResult { allowed: true, revision: rev, @@ -163,7 +166,12 @@ pub fn get_roles( if let Some(type_def) = schema.types.get(&resource_type) { // For each direct role, add its parent roles (reverse inheritance) for direct_role in &direct_roles { - add_inherited_roles(direct_role, &type_def.roles, &mut all_roles, &mut HashSet::new()); + add_inherited_roles( + direct_role, + &type_def.roles, + &mut all_roles, + &mut HashSet::new(), + ); } } diff --git a/crates/aegis-core/src/engine/scheduler.rs b/crates/aegis-core/src/engine/scheduler.rs index bc5cbb8..ce05561 100644 --- a/crates/aegis-core/src/engine/scheduler.rs +++ b/crates/aegis-core/src/engine/scheduler.rs @@ -90,7 +90,10 @@ impl GraphEngine { created_at: now.clone(), updated_at: now, }; - let mut schedules = self.analysis_schedules.lock().map_err(|e| AegisError::Internal(e.to_string()))?; + let mut schedules = self + .analysis_schedules + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; let id = schedule.id; schedules.insert(id, schedule.clone()); Ok(schedule) @@ -98,7 +101,10 @@ impl GraphEngine { /// List all analysis schedules. pub fn list_analysis_schedules(&self) -> AegisResult> { - let schedules = self.analysis_schedules.lock().map_err(|e| AegisError::Internal(e.to_string()))?; + let schedules = self + .analysis_schedules + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; let mut result: Vec<_> = schedules.values().cloned().collect(); result.sort_by(|a, b| a.created_at.cmp(&b.created_at)); Ok(result) @@ -106,14 +112,20 @@ impl GraphEngine { /// Delete an analysis schedule by ID. pub fn delete_analysis_schedule(&self, id: Uuid) -> AegisResult { - let mut schedules = self.analysis_schedules.lock().map_err(|e| AegisError::Internal(e.to_string()))?; + let mut schedules = self + .analysis_schedules + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; Ok(schedules.remove(&id).is_some()) } /// Run analysis immediately for a given schedule, or for all enabled schedules if None. pub fn run_analysis_now(&self, schedule_id: Option) -> AegisResult> { let schedules: Vec = { - let s = self.analysis_schedules.lock().map_err(|e| AegisError::Internal(e.to_string()))?; + let s = self + .analysis_schedules + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; if let Some(id) = schedule_id { s.get(&id).cloned().into_iter().collect() } else { @@ -131,7 +143,10 @@ impl GraphEngine { /// Get recent analysis runs. pub fn get_analysis_runs(&self, limit: usize) -> AegisResult> { - let runs = self.analysis_runs.lock().map_err(|e| AegisError::Internal(e.to_string()))?; + let runs = self + .analysis_runs + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; let mut result: Vec<_> = runs.values().cloned().collect(); result.sort_by(|a, b| b.completed_at.cmp(&a.completed_at)); result.truncate(limit); @@ -139,10 +154,7 @@ impl GraphEngine { } /// Start the background scheduler thread. Returns a handle that can be joined. - pub fn start_scheduler( - self: &Arc, - config: SchedulerConfig, - ) -> JoinHandle<()> { + pub fn start_scheduler(self: &Arc, config: SchedulerConfig) -> JoinHandle<()> { let engine = Arc::clone(self); std::thread::spawn(move || { let tick = std::time::Duration::from_secs(config.tick_interval_seconds); @@ -169,10 +181,16 @@ impl GraphEngine { .map(|r| { chrono::DateTime::parse_from_rfc3339(&r.completed_at) .map(|dt| dt.with_timezone(&chrono::Utc)) - .unwrap_or(now - std::time::Duration::from_secs(s.interval_seconds * 2)) + .unwrap_or( + now - std::time::Duration::from_secs( + s.interval_seconds * 2, + ), + ) }) .unwrap_or( - now - std::time::Duration::from_secs(s.interval_seconds * 2), + now - std::time::Duration::from_secs( + s.interval_seconds * 2, + ), ) }; let elapsed = (now - last_run).num_seconds() as u64; @@ -208,7 +226,10 @@ impl GraphEngine { error_message: None, }; { - let mut runs = self.analysis_runs.lock().map_err(|e| AegisError::Internal(e.to_string()))?; + let mut runs = self + .analysis_runs + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; runs.insert(run_id, run); } @@ -235,7 +256,10 @@ impl GraphEngine { }; { - let mut runs = self.analysis_runs.lock().map_err(|e| AegisError::Internal(e.to_string()))?; + let mut runs = self + .analysis_runs + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; runs.insert(run_id, completed_run.clone()); } @@ -269,12 +293,18 @@ impl GraphEngine { "allowed": check.allowed, })); } - output.insert("checks".to_string(), serde_json::Value::Array(check_results)); + output.insert( + "checks".to_string(), + serde_json::Value::Array(check_results), + ); // Run access diff if a compare schema is provided if let Some(ref compare_schema) = schedule.compare_schema { let current_schema = { - let s = self.schema.read().map_err(|e| AegisError::Internal(e.to_string()))?; + let s = self + .schema + .read() + .map_err(|e| AegisError::Internal(e.to_string()))?; s.clone() }; match self.access_diff(¤t_schema, compare_schema, None, Some(1000)) { @@ -304,13 +334,13 @@ impl GraphEngine { } } -#[cfg(test)] +#[cfg(all(test, feature = "sqlite"))] mod tests { use super::*; use crate::engine::GraphEngine; + use crate::storage::StorageBackend; #[cfg(feature = "sqlite")] use crate::storage::sqlite::{SqliteConfig, SqliteStorage}; - use crate::storage::StorageBackend; use crate::types::*; use std::sync::Arc; diff --git a/crates/aegis-core/src/engine/traversal.rs b/crates/aegis-core/src/engine/traversal.rs index bf47ddc..7967eb6 100644 --- a/crates/aegis-core/src/engine/traversal.rs +++ b/crates/aegis-core/src/engine/traversal.rs @@ -1,9 +1,11 @@ -use chrono::Utc; use crate::engine::cache::TraversalCache; use crate::engine::condition::{self, ConditionEvalContext}; use crate::error::{AegisError, AegisResult}; use crate::storage::StorageBackend; -use crate::types::{ConsistencyMode, PartitionId, Relation, ResourceId, Revision, SubjectId, SubjectSet}; +use crate::types::{ + ConsistencyMode, PartitionId, Relation, ResourceId, Revision, SubjectId, SubjectSet, +}; +use chrono::Utc; use std::collections::{HashSet, VecDeque}; /// A single step in a traversal trace. @@ -50,10 +52,24 @@ pub fn bfs_traversal( revision: Option, consistency: Option, ) -> AegisResult { - bfs_traversal_with_limits_and_context(partition_id, storage, subject, relation, target, revision, consistency, DEFAULT_MAX_DEPTH, DEFAULT_MAX_VISITS, None, None, None) + bfs_traversal_with_limits_and_context( + partition_id, + storage, + subject, + relation, + target, + revision, + consistency, + DEFAULT_MAX_DEPTH, + DEFAULT_MAX_VISITS, + None, + None, + None, + ) } /// BFS traversal with context for tuple condition evaluation. +#[allow(clippy::too_many_arguments)] pub fn bfs_traversal_with_context( partition_id: &PartitionId, storage: &dyn StorageBackend, @@ -64,10 +80,24 @@ pub fn bfs_traversal_with_context( consistency: Option, context: Option<&ConditionEvalContext>, ) -> AegisResult { - bfs_traversal_with_limits_and_context(partition_id, storage, subject, relation, target, revision, consistency, DEFAULT_MAX_DEPTH, DEFAULT_MAX_VISITS, None, context, None) + bfs_traversal_with_limits_and_context( + partition_id, + storage, + subject, + relation, + target, + revision, + consistency, + DEFAULT_MAX_DEPTH, + DEFAULT_MAX_VISITS, + None, + context, + None, + ) } /// BFS traversal with configurable depth and visit limits. +#[allow(clippy::too_many_arguments)] pub fn bfs_traversal_with_limits( partition_id: &PartitionId, storage: &dyn StorageBackend, @@ -81,8 +111,18 @@ pub fn bfs_traversal_with_limits( cache: Option<&mut TraversalCache>, ) -> AegisResult { bfs_traversal_with_limits_and_context( - partition_id, storage, subject, relation, target, revision, consistency, - max_depth, max_visits, cache, None, None, + partition_id, + storage, + subject, + relation, + target, + revision, + consistency, + max_depth, + max_visits, + cache, + None, + None, ) } @@ -90,6 +130,7 @@ pub fn bfs_traversal_with_limits( /// /// `per_branch_max_visits` limits how many tuples each branch may explore before /// being pruned. This prevents a single deep branch from dominating the traversal budget. +#[allow(clippy::too_many_arguments)] pub fn bfs_traversal_with_limits_and_context( partition_id: &PartitionId, storage: &dyn StorageBackend, @@ -104,15 +145,26 @@ pub fn bfs_traversal_with_limits_and_context( context: Option<&ConditionEvalContext>, per_branch_max_visits: Option, ) -> AegisResult { - let consistency_ref = consistency.as_ref().unwrap_or(&ConsistencyMode::MinimizeLatency); + let consistency_ref = consistency + .as_ref() + .unwrap_or(&ConsistencyMode::MinimizeLatency); let mut visited: HashSet<(String, String)> = HashSet::new(); let mut queue: VecDeque<(SubjectId, Vec)> = VecDeque::new(); let mut visit_count = 0usize; - let mut per_branch_counts: std::collections::HashMap = std::collections::HashMap::new(); + let mut per_branch_counts: std::collections::HashMap = + std::collections::HashMap::new(); let per_branch_limit = per_branch_max_visits.unwrap_or(usize::MAX); - let found_direct = check_direct(partition_id, storage, subject, relation, target, consistency_ref, context)?; + let found_direct = check_direct( + partition_id, + storage, + subject, + relation, + target, + consistency_ref, + context, + )?; if found_direct { return Ok(TraversalResult { found: true, @@ -133,7 +185,9 @@ pub fn bfs_traversal_with_limits_and_context( let tuples = { let current_rev = revision.unwrap_or(Revision::ZERO); if let Some(ref mut c) = cache { - if let Some(cached_objects) = c.get(current_subject.as_str(), relation.as_str(), current_rev) { + if let Some(cached_objects) = + c.get(current_subject.as_str(), relation.as_str(), current_rev) + { let mut result = Vec::new(); for obj_str in &cached_objects { if let Ok(obj) = ResourceId::new(obj_str) { @@ -146,15 +200,35 @@ pub fn bfs_traversal_with_limits_and_context( } result } else { - let tuples = load_tuples(partition_id, storage, ¤t_subject, relation, consistency_ref)?; - let objects: Vec = tuples.iter().map(|t| t.object.as_str().to_string()).collect(); + let tuples = load_tuples( + partition_id, + storage, + ¤t_subject, + relation, + consistency_ref, + )?; + let objects: Vec = tuples + .iter() + .map(|t| t.object.as_str().to_string()) + .collect(); if !objects.is_empty() { - c.insert(current_subject.as_str(), relation.as_str(), objects, current_rev); + c.insert( + current_subject.as_str(), + relation.as_str(), + objects, + current_rev, + ); } tuples } } else { - load_tuples(partition_id, storage, ¤t_subject, relation, consistency_ref)? + load_tuples( + partition_id, + storage, + ¤t_subject, + relation, + consistency_ref, + )? } }; @@ -190,14 +264,22 @@ pub fn bfs_traversal_with_limits_and_context( // Subject-set resolution: if the tuple's subject is a subject-set // (e.g. "team:eng#member"), we need to verify that our original // traversal subject satisfies the subject-set condition. + #[allow(clippy::collapsible_if)] if let Some(ref subject_set) = tuple.subject.as_subject_set() { - if !is_subject_set_member(partition_id, storage, subject, subject_set, consistency_ref, context)? { + if !is_subject_set_member( + partition_id, + storage, + subject, + subject_set, + consistency_ref, + context, + )? { continue; } - // For subject-set tuples, the edge still goes from current_subject - // (which equals subject_set.object) to tuple.object via tuple.relation. - // We continue processing normally below. } + // For subject-set tuples, the edge still goes from current_subject + // (which equals subject_set.object) to tuple.object via tuple.relation. + // We continue processing normally below. let object_str = tuple.object.as_str().to_string(); @@ -260,16 +342,17 @@ fn load_tuples( consistency: &ConsistencyMode, ) -> AegisResult> { // 1. Direct subject match (existing behavior) - let mut tuples = match storage.list_by_subject(partition_id, current_subject, Some(relation), consistency) { - Ok(t) => t, - Err(e) => { - if matches!(e, AegisError::StorageNotInitialized) { - Vec::new() - } else { - return Err(e); + let mut tuples = + match storage.list_by_subject(partition_id, current_subject, Some(relation), consistency) { + Ok(t) => t, + Err(e) => { + if matches!(e, AegisError::StorageNotInitialized) { + Vec::new() + } else { + return Err(e); + } } - } - }; + }; // 2. Subject-set match: find tuples where subject is `{current_subject}#{relation}` // e.g. if current_subject = team:eng, find tuples with subject = team:eng#member @@ -281,7 +364,8 @@ fn load_tuples( } else { return Ok(tuples); }; - let set_tuples = storage.list_by_subject_set_of(partition_id, &set_object, Some(relation), consistency); + let set_tuples = + storage.list_by_subject_set_of(partition_id, &set_object, Some(relation), consistency); if let Ok(mut st) = set_tuples { tuples.append(&mut st); } @@ -316,8 +400,16 @@ fn check_direct( return Ok(true); } // Subject-set match: subject is like `team:eng#member` + #[allow(clippy::collapsible_if)] if let Some(ref subject_set) = t.subject.as_subject_set() { - if is_subject_set_member(partition_id, storage, subject, subject_set, consistency, context)? { + if is_subject_set_member( + partition_id, + storage, + subject, + subject_set, + consistency, + context, + )? { return Ok(true); } } @@ -335,11 +427,16 @@ fn is_subject_set_member( consistency: &ConsistencyMode, context: Option<&ConditionEvalContext>, ) -> AegisResult { - let tuples = storage.list_by_object(partition_id, &subject_set.object, Some(&subject_set.relation), consistency)?; + let tuples = storage.list_by_object( + partition_id, + &subject_set.object, + Some(&subject_set.relation), + consistency, + )?; let now = Utc::now(); Ok(tuples.iter().any(|t| { t.subject == *subject - && t.valid_until.map_or(true, |v| v > now) + && t.valid_until.is_none_or(|v| v > now) && evaluate_tuple_condition(&t.condition, context) })) } diff --git a/crates/aegis-core/src/engine/watch.rs b/crates/aegis-core/src/engine/watch.rs index 04e9ad4..f2f95cf 100644 --- a/crates/aegis-core/src/engine/watch.rs +++ b/crates/aegis-core/src/engine/watch.rs @@ -46,21 +46,25 @@ pub struct WatchFilter { impl WatchFilter { pub fn matches(&self, event: &WatchEvent) -> bool { + #[allow(clippy::collapsible_if)] if let Some(subjects) = &self.subjects { if !subjects.iter().any(|s| s == &event.subject) { return false; } } + #[allow(clippy::collapsible_if)] if let Some(relations) = &self.relations { if !relations.iter().any(|r| r == &event.relation) { return false; } } + #[allow(clippy::collapsible_if)] if let Some(objects) = &self.objects { if !objects.iter().any(|o| o == &event.object) { return false; } } + #[allow(clippy::collapsible_if)] if let Some(types) = &self.event_types { if !types.contains(&event.event_type) { return false; @@ -145,9 +149,9 @@ impl Drop for WatchSubscription { mod tests { use super::*; use crate::engine::GraphEngine; + use crate::storage::StorageBackend; #[cfg(feature = "sqlite")] use crate::storage::sqlite::{SqliteConfig, SqliteStorage}; - use crate::storage::StorageBackend; use crate::types::*; use std::sync::mpsc::TryRecvError; diff --git a/crates/aegis-core/src/error.rs b/crates/aegis-core/src/error.rs index 28769cb..1f5dba3 100644 --- a/crates/aegis-core/src/error.rs +++ b/crates/aegis-core/src/error.rs @@ -192,5 +192,4 @@ mod tests { "revision token from a different node is incompatible with single-node mode" ); } - } diff --git a/crates/aegis-core/src/lib.rs b/crates/aegis-core/src/lib.rs index af2280d..3b3a9e3 100644 --- a/crates/aegis-core/src/lib.rs +++ b/crates/aegis-core/src/lib.rs @@ -20,15 +20,21 @@ pub mod testing; pub mod types; pub mod util; +pub use crate::engine::GraphEngine; +pub use crate::engine::condition::ConditionEvalContext; +pub use crate::engine::enforcement_history::{ + EnforcementEvent, EnforcementHistoryConfig, EnforcementTrends, SamplingMode, +}; /// Re-export the most commonly used types at the crate root. pub use crate::engine::gdpr::{GdprConfig, GdprManager, SubjectDataExport}; +pub use crate::engine::policy_lifecycle::{ + DraftStatus, PolicyDraft, PublishResult, ValidationReport, +}; pub use crate::engine::ratelimit::{RateLimitConfig, RateLimitOp, TokenBucketRateLimiter}; -pub use crate::engine::condition::ConditionEvalContext; -pub use crate::engine::enforcement_history::{EnforcementEvent, EnforcementHistoryConfig, EnforcementTrends, SamplingMode}; -pub use crate::engine::policy_lifecycle::{DraftStatus, PolicyDraft, PublishResult, ValidationReport}; -pub use crate::engine::scheduler::{AnalysisRun, AnalysisRunStatus, AnalysisSchedule, SchedulerConfig}; +pub use crate::engine::scheduler::{ + AnalysisRun, AnalysisRunStatus, AnalysisSchedule, SchedulerConfig, +}; pub use crate::engine::watch::{WatchEvent, WatchEventType, WatchFilter, WatchSubscription}; -pub use crate::engine::GraphEngine; pub use crate::error::{AegisError, AegisResult}; pub use crate::types::{ AccessReviewEntry, AuditEntry, CheckResult, ConsistencyMode, ExplainResult, ExplainTrace, @@ -144,7 +150,8 @@ mod integration_tests { assert!(!cross.allowed); // Each tenant has its own tuples - let alpha_tuples = aegis.list_by_subject(&SubjectId::new("user:alpha1").unwrap(), None, None); + let alpha_tuples = + aegis.list_by_subject(&SubjectId::new("user:alpha1").unwrap(), None, None); assert_eq!(alpha_tuples.len(), 1); assert_eq!(alpha_tuples[0].object.as_str(), "tenant:alpha"); } diff --git a/crates/aegis-core/src/schema/mod.rs b/crates/aegis-core/src/schema/mod.rs index bb92b13..664e633 100644 --- a/crates/aegis-core/src/schema/mod.rs +++ b/crates/aegis-core/src/schema/mod.rs @@ -5,9 +5,5 @@ mod validator; pub use parser::parse_schema; pub use types::*; pub use validator::{ - check_schema_compatibility, - lint_schema, - LintReport, - validate_relation, - validate_resource_type, + LintReport, check_schema_compatibility, lint_schema, validate_relation, validate_resource_type, }; diff --git a/crates/aegis-core/src/schema/parser.rs b/crates/aegis-core/src/schema/parser.rs index 4d1e36c..47fe306 100644 --- a/crates/aegis-core/src/schema/parser.rs +++ b/crates/aegis-core/src/schema/parser.rs @@ -209,6 +209,7 @@ pub fn lint_schema(schema: &Schema) -> LintResult { // Check condition syntax on permissions for (perm_name, perm_def) in &type_def.permissions { + #[allow(clippy::collapsible_if)] if let Some(ref cond) = perm_def.condition { if let Err(e) = crate::engine::condition::parse_condition(cond) { diagnostics.push(LintDiagnostic { @@ -274,14 +275,25 @@ pub fn lint_schema(schema: &Schema) -> LintResult { if !has_content { diagnostics.push(LintDiagnostic { severity: LintSeverity::Warning, - message: format!("type '{type_name}' is defined but has no relations or permissions"), + message: format!( + "type '{type_name}' is defined but has no relations or permissions" + ), location: Some(format!("types.{type_name}")), }); } else if schema.types.len() > 1 { - let is_referenced = schema.types.iter().filter(|(k, _)| *k != type_name).any(|(_, t)| { - t.relations.values().any(|r| r.inherit_from.iter().any(|s| s == type_name)) - || t.permissions.values().any(|p| p.union_of.iter().any(|s| s == type_name)) - }); + let is_referenced = + schema + .types + .iter() + .filter(|(k, _)| *k != type_name) + .any(|(_, t)| { + t.relations + .values() + .any(|r| r.inherit_from.iter().any(|s| s == type_name)) + || t.permissions + .values() + .any(|p| p.union_of.iter().any(|s| s == type_name)) + }); if !is_referenced { diagnostics.push(LintDiagnostic { severity: LintSeverity::Warning, @@ -292,7 +304,7 @@ pub fn lint_schema(schema: &Schema) -> LintResult { }); } } - } + } } LintResult::with_diagnostics(diagnostics) @@ -334,10 +346,10 @@ fn has_circular_relations( for rel_def in type_def.relations.values() { for inherit_ref in &rel_def.inherit_from { // Check if the reference is a type name (not a relation pattern) - if types.contains_key(inherit_ref) { - if has_circular_relations(inherit_ref, types, visited) { - return true; - } + if types.contains_key(inherit_ref) + && has_circular_relations(inherit_ref, types, visited) + { + return true; } } } @@ -497,14 +509,26 @@ types: .iter() .filter(|d| d.message.contains("never referenced")) .collect(); - assert_eq!(orphan_warnings.len(), 1, "expected 1 orphan warning, got {}: {:?}", orphan_warnings.len(), orphan_warnings); + assert_eq!( + orphan_warnings.len(), + 1, + "expected 1 orphan warning, got {}: {:?}", + orphan_warnings.len(), + orphan_warnings + ); // With only one type and it has relations/permissions, no unused type warning let unused_types: Vec<_> = result .diagnostics .iter() .filter(|d| d.message.contains("never referenced")) .collect(); - assert_eq!(unused_types.len(), 1, "expected 1 orphan relation warning, got {}: {:?}", unused_types.len(), unused_types); + assert_eq!( + unused_types.len(), + 1, + "expected 1 orphan relation warning, got {}: {:?}", + unused_types.len(), + unused_types + ); } #[test] diff --git a/crates/aegis-core/src/schema/types.rs b/crates/aegis-core/src/schema/types.rs index 8328df1..06faf2c 100644 --- a/crates/aegis-core/src/schema/types.rs +++ b/crates/aegis-core/src/schema/types.rs @@ -39,5 +39,3 @@ impl LintResult { } } } - - diff --git a/crates/aegis-core/src/schema/validator.rs b/crates/aegis-core/src/schema/validator.rs index 8fb67f8..5271731 100644 --- a/crates/aegis-core/src/schema/validator.rs +++ b/crates/aegis-core/src/schema/validator.rs @@ -25,17 +25,30 @@ pub fn lint_schema(schema: &Schema, strict: bool) -> LintReport { for (type_name, type_def) in &schema.types { // Check for missing documentation on relations for (rel_name, rel_def) in &type_def.relations { - if rel_def.description.is_none() || rel_def.description.as_deref().unwrap_or("").is_empty() { + if rel_def.description.is_none() + || rel_def.description.as_deref().unwrap_or("").is_empty() + { let msg = format!("relation '{rel_name}' on type '{type_name}' has no description"); - if strict { errors.push(msg); } else { warnings.push(msg); } + if strict { + errors.push(msg); + } else { + warnings.push(msg); + } } } // Check for missing documentation on permissions for (perm_name, perm_def) in &type_def.permissions { - if perm_def.description.is_none() || perm_def.description.as_deref().unwrap_or("").is_empty() { - let msg = format!("permission '{perm_name}' on type '{type_name}' has no description"); - if strict { errors.push(msg); } else { warnings.push(msg); } + if perm_def.description.is_none() + || perm_def.description.as_deref().unwrap_or("").is_empty() + { + let msg = + format!("permission '{perm_name}' on type '{type_name}' has no description"); + if strict { + errors.push(msg); + } else { + warnings.push(msg); + } } } @@ -43,8 +56,14 @@ pub fn lint_schema(schema: &Schema, strict: bool) -> LintReport { for (perm_name, perm_def) in &type_def.permissions { let combined = perm_def.union_of.join(" "); if combined.contains('*') { - let msg = format!("permission '{perm_name}' on type '{type_name}' uses wildcard '*' — overly broad"); - if strict { errors.push(msg); } else { warnings.push(msg); } + let msg = format!( + "permission '{perm_name}' on type '{type_name}' uses wildcard '*' — overly broad" + ); + if strict { + errors.push(msg); + } else { + warnings.push(msg); + } } } @@ -53,8 +72,14 @@ pub fn lint_schema(schema: &Schema, strict: bool) -> LintReport { if perm_def.effect == Effect::Deny { for rel_ref in &perm_def.union_of { if !type_def.relations.contains_key(rel_ref) { - let msg = format!("deny permission '{perm_name}' on type '{type_name}' references undefined relation '{rel_ref}'"); - if strict { errors.push(msg); } else { warnings.push(msg); } + let msg = format!( + "deny permission '{perm_name}' on type '{type_name}' references undefined relation '{rel_ref}'" + ); + if strict { + errors.push(msg); + } else { + warnings.push(msg); + } } } } @@ -63,17 +88,29 @@ pub fn lint_schema(schema: &Schema, strict: bool) -> LintReport { // Check for empty roles (no permissions) — generate a warning for (role_name, role_def) in &type_def.roles { if role_def.permissions.is_empty() { - let msg = format!("role '{role_name}' on type '{type_name}' has no permissions assigned"); - if strict { errors.push(msg); } else { warnings.push(msg); } + let msg = + format!("role '{role_name}' on type '{type_name}' has no permissions assigned"); + if strict { + errors.push(msg); + } else { + warnings.push(msg); + } } } // Check condition syntax validity on permissions for (perm_name, perm_def) in &type_def.permissions { + #[allow(clippy::collapsible_if)] if let Some(ref cond) = perm_def.condition { if let Err(e) = crate::engine::condition::parse_condition(cond) { - let msg = format!("permission '{perm_name}' on type '{type_name}' has invalid condition syntax: {e}"); - if strict { errors.push(msg); } else { warnings.push(msg); } + let msg = format!( + "permission '{perm_name}' on type '{type_name}' has invalid condition syntax: {e}" + ); + if strict { + errors.push(msg); + } else { + warnings.push(msg); + } } } } @@ -85,15 +122,34 @@ pub fn lint_schema(schema: &Schema, strict: bool) -> LintReport { let has_content = !type_def.relations.is_empty() || !type_def.permissions.is_empty(); if !has_content { let msg = format!("type '{type_name}' is defined but has no relations or permissions"); - if strict { errors.push(msg); } else { warnings.push(msg); } + if strict { + errors.push(msg); + } else { + warnings.push(msg); + } } else if schema.types.len() > 1 { - let is_referenced = schema.types.iter().filter(|(k, _)| *k != type_name).any(|(_, t)| { - t.relations.values().any(|r| r.inherit_from.iter().any(|s| s == type_name)) - || t.permissions.values().any(|p| p.union_of.iter().any(|s| s == type_name)) - }); + let is_referenced = + schema + .types + .iter() + .filter(|(k, _)| *k != type_name) + .any(|(_, t)| { + t.relations + .values() + .any(|r| r.inherit_from.iter().any(|s| s == type_name)) + || t.permissions + .values() + .any(|p| p.union_of.iter().any(|s| s == type_name)) + }); if !is_referenced { - let msg = format!("type '{type_name}' is defined but never referenced by any other type's relations or permissions"); - if strict { errors.push(msg); } else { warnings.push(msg); } + let msg = format!( + "type '{type_name}' is defined but never referenced by any other type's relations or permissions" + ); + if strict { + errors.push(msg); + } else { + warnings.push(msg); + } } } } @@ -173,7 +229,7 @@ pub fn validate_resource_type(schema: &Schema, resource: &str) -> AegisResult<() let type_name = resource .split(':') .next() - .ok_or_else(|| AegisError::Validation(crate::types::ValidationError::Empty))?; + .ok_or(AegisError::Validation(crate::types::ValidationError::Empty))?; if !schema.types.contains_key(type_name) { return Err(AegisError::UnknownSubjectType(type_name.to_string())); @@ -186,7 +242,7 @@ pub fn validate_relation(schema: &Schema, resource: &str, relation: &str) -> Aeg let type_name = resource .split(':') .next() - .ok_or_else(|| AegisError::Validation(crate::types::ValidationError::Empty))?; + .ok_or(AegisError::Validation(crate::types::ValidationError::Empty))?; if !schema.has_relation(type_name, relation) && !schema.has_permission(type_name, relation) { return Err(AegisError::UnknownRelation { @@ -300,7 +356,11 @@ types: "#, ); let report = lint_schema(&schema, false); - assert!(report.is_clean(), "expected clean lint: {:?}", report.warnings); + assert!( + report.is_clean(), + "expected clean lint: {:?}", + report.warnings + ); } #[test] @@ -319,42 +379,71 @@ types: ); let report = lint_schema(&schema, false); assert!(!report.warnings.is_empty()); - assert!(report.warnings.iter().any(|w| w.contains("owner") && w.contains("description"))); - assert!(report.warnings.iter().any(|w| w.contains("read") && w.contains("description"))); + assert!( + report + .warnings + .iter() + .any(|w| w.contains("owner") && w.contains("description")) + ); + assert!( + report + .warnings + .iter() + .any(|w| w.contains("read") && w.contains("description")) + ); } - - #[test] fn lint_schema_condition_syntax() { use crate::types::schema::{PermissionDef, RelationDef, TypeDef}; let mut types = std::collections::HashMap::new(); let mut relations = std::collections::HashMap::new(); - relations.insert("owner".to_string(), RelationDef { - inherit_from: vec!["user".to_string()], - description: Some("owner".to_string()), - }); + relations.insert( + "owner".to_string(), + RelationDef { + inherit_from: vec!["user".to_string()], + description: Some("owner".to_string()), + }, + ); let mut permissions = std::collections::HashMap::new(); - permissions.insert("admin".to_string(), PermissionDef { - union_of: vec!["owner".to_string()], - condition: Some("role eq admin".to_string()), - description: Some("admin".to_string()), - ..Default::default() - }); - permissions.insert("invalid".to_string(), PermissionDef { - union_of: vec!["owner".to_string()], - condition: Some("bad syntax here".to_string()), - description: Some("invalid".to_string()), - ..Default::default() - }); - types.insert("repo".to_string(), TypeDef { relations, permissions, ..Default::default() }); + permissions.insert( + "admin".to_string(), + PermissionDef { + union_of: vec!["owner".to_string()], + condition: Some("role eq admin".to_string()), + description: Some("admin".to_string()), + ..Default::default() + }, + ); + permissions.insert( + "invalid".to_string(), + PermissionDef { + union_of: vec!["owner".to_string()], + condition: Some("bad syntax here".to_string()), + description: Some("invalid".to_string()), + ..Default::default() + }, + ); + types.insert( + "repo".to_string(), + TypeDef { + relations, + permissions, + ..Default::default() + }, + ); let schema = Schema { schema_version: 1, namespace: "test".to_string(), types, }; let report = lint_schema(&schema, false); - assert!(report.warnings.iter().any(|w| w.contains("condition")), "expected condition syntax warning: errors={:?} warnings={:?}", report.errors, report.warnings); + assert!( + report.warnings.iter().any(|w| w.contains("condition")), + "expected condition syntax warning: errors={:?} warnings={:?}", + report.errors, + report.warnings + ); } #[test] diff --git a/crates/aegis-core/src/storage/async_traits.rs b/crates/aegis-core/src/storage/async_traits.rs index 3e1db93..fefe26f 100644 --- a/crates/aegis-core/src/storage/async_traits.rs +++ b/crates/aegis-core/src/storage/async_traits.rs @@ -3,14 +3,14 @@ use std::sync::{Arc, Mutex}; use async_trait::async_trait; use chrono::{DateTime, Utc}; +use crate::engine::enforcement_history::EnforcementEvent; +use crate::engine::policy_lifecycle::PolicyDraft; +use crate::engine::scheduler::{AnalysisRun, AnalysisSchedule}; use crate::error::{AegisError, AegisResult}; use crate::storage::memory::InMemoryStorage; use crate::storage::traits::{ BackendType, IntegrityReport, StorageBackend, StorageMeta, TupleFilter, }; -use crate::engine::enforcement_history::EnforcementEvent; -use crate::engine::policy_lifecycle::PolicyDraft; -use crate::engine::scheduler::{AnalysisRun, AnalysisSchedule}; use crate::types::{ AuditEntry, ConsistencyMode, PaginatedTuples, PaginationParams, PartitionId, Relation, RelationshipTuple, ResourceId, Revision, RevisionToken, SubjectId, TupleKey, TupleMutation, @@ -70,8 +70,11 @@ impl StorageCapabilities { /// Async storage transaction supporting atomic multi-tuple writes within a partition. #[async_trait(?Send)] pub trait AsyncStorageTransaction: Send { - async fn write(&mut self, partition_id: &PartitionId, tuple: &RelationshipTuple) - -> AegisResult<()>; + async fn write( + &mut self, + partition_id: &PartitionId, + tuple: &RelationshipTuple, + ) -> AegisResult<()>; async fn delete(&mut self, partition_id: &PartitionId, key: &TupleKey) -> AegisResult<()>; @@ -81,10 +84,7 @@ pub trait AsyncStorageTransaction: Send { async fn release_savepoint(&self, name: &str) -> AegisResult<()>; - async fn set_actor_identity( - &mut self, - identity: Option, - ) -> Option { + async fn set_actor_identity(&mut self, identity: Option) -> Option { let _ = identity; None } @@ -134,11 +134,7 @@ pub trait AsyncStorageBackend: Send + Sync { object: &ResourceId, ) -> AegisResult; - async fn has_tuple( - &self, - partition_id: &PartitionId, - key: &TupleKey, - ) -> AegisResult; + async fn has_tuple(&self, partition_id: &PartitionId, key: &TupleKey) -> AegisResult; async fn read_tuple( &self, @@ -203,10 +199,7 @@ pub trait AsyncStorageBackend: Send + Sync { consistency: &ConsistencyMode, ) -> AegisResult; - async fn current_revision( - &self, - partition_id: &PartitionId, - ) -> AegisResult; + async fn current_revision(&self, partition_id: &PartitionId) -> AegisResult; async fn read_schema_version(&self) -> AegisResult; @@ -236,10 +229,7 @@ pub trait AsyncStorageBackend: Send + Sync { cutoff: DateTime, ) -> AegisResult; - async fn compact_events( - &self, - partition_id: &PartitionId, - ) -> AegisResult; + async fn compact_events(&self, partition_id: &PartitionId) -> AegisResult; async fn delete_soft_deleted_tuples_before( &self, @@ -265,44 +255,52 @@ pub trait AsyncStorageBackend: Send + Sync { None } - async fn set_actor_identity( - &self, - identity: Option, - ) -> Option { + async fn set_actor_identity(&self, identity: Option) -> Option { let _ = identity; None } async fn close(&self) -> AegisResult<()>; - async fn verify_audit_chain( - &self, - partition_id: &PartitionId, - ) -> AegisResult> { + async fn verify_audit_chain(&self, partition_id: &PartitionId) -> AegisResult> { let _ = partition_id; Ok(None) } async fn save_policy_draft(&self, _draft: &PolicyDraft) -> AegisResult<()> { - Err(AegisError::UnsupportedStorageOperation("async save_policy_draft not supported".into())) + Err(AegisError::UnsupportedStorageOperation( + "async save_policy_draft not supported".into(), + )) } async fn load_policy_draft(&self, _id: &str) -> AegisResult> { - Err(AegisError::UnsupportedStorageOperation("async load_policy_draft not supported".into())) + Err(AegisError::UnsupportedStorageOperation( + "async load_policy_draft not supported".into(), + )) } async fn delete_policy_draft(&self, _id: &str) -> AegisResult { - Err(AegisError::UnsupportedStorageOperation("async delete_policy_draft not supported".into())) + Err(AegisError::UnsupportedStorageOperation( + "async delete_policy_draft not supported".into(), + )) } async fn save_analysis_schedule(&self, _schedule: &AnalysisSchedule) -> AegisResult<()> { - Err(AegisError::UnsupportedStorageOperation("async save_analysis_schedule not supported".into())) + Err(AegisError::UnsupportedStorageOperation( + "async save_analysis_schedule not supported".into(), + )) } async fn delete_analysis_schedule(&self, _id: &str) -> AegisResult { - Err(AegisError::UnsupportedStorageOperation("async delete_analysis_schedule not supported".into())) + Err(AegisError::UnsupportedStorageOperation( + "async delete_analysis_schedule not supported".into(), + )) } async fn save_analysis_run(&self, _run: &AnalysisRun) -> AegisResult<()> { - Err(AegisError::UnsupportedStorageOperation("async save_analysis_run not supported".into())) + Err(AegisError::UnsupportedStorageOperation( + "async save_analysis_run not supported".into(), + )) } async fn save_enforcement_event(&self, _event: &EnforcementEvent) -> AegisResult<()> { - Err(AegisError::UnsupportedStorageOperation("async save_enforcement_event not supported".into())) + Err(AegisError::UnsupportedStorageOperation( + "async save_enforcement_event not supported".into(), + )) } } @@ -312,6 +310,12 @@ pub struct InMemoryAsyncStorage { storage: Arc>, } +impl Default for InMemoryAsyncStorage { + fn default() -> Self { + Self::new() + } +} + impl InMemoryAsyncStorage { pub fn new() -> Self { let mut inner = InMemoryStorage::new(); @@ -322,8 +326,12 @@ impl InMemoryAsyncStorage { } } -fn lock_storage(storage: &Arc>) -> AegisResult> { - storage.lock().map_err(|e| crate::error::AegisError::Internal(e.to_string())) +fn lock_storage( + storage: &Arc>, +) -> AegisResult> { + storage + .lock() + .map_err(|e| crate::error::AegisError::Internal(e.to_string())) } #[async_trait(?Send)] @@ -376,11 +384,7 @@ impl AsyncStorageBackend for InMemoryAsyncStorage { lock_storage(&self.storage)?.delete_object(partition_id, object) } - async fn has_tuple( - &self, - partition_id: &PartitionId, - key: &TupleKey, - ) -> AegisResult { + async fn has_tuple(&self, partition_id: &PartitionId, key: &TupleKey) -> AegisResult { lock_storage(&self.storage)?.has_tuple(partition_id, key) } @@ -399,8 +403,7 @@ impl AsyncStorageBackend for InMemoryAsyncStorage { relation: Option<&Relation>, consistency: &ConsistencyMode, ) -> AegisResult> { - lock_storage(&self.storage)? - .list_by_object(partition_id, object, relation, consistency) + lock_storage(&self.storage)?.list_by_object(partition_id, object, relation, consistency) } async fn list_by_subject( @@ -410,8 +413,7 @@ impl AsyncStorageBackend for InMemoryAsyncStorage { relation: Option<&Relation>, consistency: &ConsistencyMode, ) -> AegisResult> { - lock_storage(&self.storage)? - .list_by_subject(partition_id, subject, relation, consistency) + lock_storage(&self.storage)?.list_by_subject(partition_id, subject, relation, consistency) } async fn list_by_relation( @@ -420,8 +422,7 @@ impl AsyncStorageBackend for InMemoryAsyncStorage { object: &ResourceId, relation: &Relation, ) -> AegisResult> { - lock_storage(&self.storage)? - .list_by_relation(partition_id, object, relation) + lock_storage(&self.storage)?.list_by_relation(partition_id, object, relation) } async fn query_tuples( @@ -431,14 +432,10 @@ impl AsyncStorageBackend for InMemoryAsyncStorage { pagination: &PaginationParams, consistency: &ConsistencyMode, ) -> AegisResult { - lock_storage(&self.storage)? - .query_tuples(partition_id, filter, pagination, consistency) + lock_storage(&self.storage)?.query_tuples(partition_id, filter, pagination, consistency) } - async fn current_revision( - &self, - partition_id: &PartitionId, - ) -> AegisResult { + async fn current_revision(&self, partition_id: &PartitionId) -> AegisResult { lock_storage(&self.storage)?.current_revision(partition_id) } @@ -472,8 +469,13 @@ impl AsyncStorageBackend for InMemoryAsyncStorage { to_revision: Option, pagination: &PaginationParams, ) -> AegisResult> { - lock_storage(&self.storage)? - .query_audit(partition_id, object, from_revision, to_revision, pagination) + lock_storage(&self.storage)?.query_audit( + partition_id, + object, + from_revision, + to_revision, + pagination, + ) } async fn integrity_check(&self) -> AegisResult { @@ -488,10 +490,7 @@ impl AsyncStorageBackend for InMemoryAsyncStorage { lock_storage(&self.storage)?.delete_events_before(partition_id, cutoff) } - async fn compact_events( - &self, - partition_id: &PartitionId, - ) -> AegisResult { + async fn compact_events(&self, partition_id: &PartitionId) -> AegisResult { lock_storage(&self.storage)?.compact_events(partition_id) } @@ -500,8 +499,7 @@ impl AsyncStorageBackend for InMemoryAsyncStorage { partition_id: &PartitionId, cutoff: DateTime, ) -> AegisResult { - lock_storage(&self.storage)? - .delete_soft_deleted_tuples_before(partition_id, cutoff) + lock_storage(&self.storage)?.delete_soft_deleted_tuples_before(partition_id, cutoff) } async fn recover_from_events( @@ -519,25 +517,20 @@ impl AsyncStorageBackend for InMemoryAsyncStorage { events: &[AuditEntry], revision: Revision, ) -> AegisResult<()> { - lock_storage(&self.storage)? - .restore_backup(partition_id, tuples, events, revision) + lock_storage(&self.storage)?.restore_backup(partition_id, tuples, events, revision) } - async fn set_actor_identity( - &self, - identity: Option, - ) -> Option { - lock_storage(&self.storage).ok()?.set_actor_identity(identity) + async fn set_actor_identity(&self, identity: Option) -> Option { + lock_storage(&self.storage) + .ok()? + .set_actor_identity(identity) } async fn close(&self) -> AegisResult<()> { lock_storage(&self.storage)?.close() } - async fn verify_audit_chain( - &self, - partition_id: &PartitionId, - ) -> AegisResult> { + async fn verify_audit_chain(&self, partition_id: &PartitionId) -> AegisResult> { lock_storage(&self.storage)?.verify_audit_chain(partition_id) } @@ -581,11 +574,7 @@ impl AsyncStorageTransaction for InMemoryAsyncTransaction { Ok(()) } - async fn delete( - &mut self, - partition_id: &PartitionId, - key: &TupleKey, - ) -> AegisResult<()> { + async fn delete(&mut self, partition_id: &PartitionId, key: &TupleKey) -> AegisResult<()> { let tuple = RelationshipTuple::new( key.subject.clone(), key.relation.clone(), diff --git a/crates/aegis-core/src/storage/indexeddb.rs b/crates/aegis-core/src/storage/indexeddb.rs index 05df1d9..afe5927 100644 --- a/crates/aegis-core/src/storage/indexeddb.rs +++ b/crates/aegis-core/src/storage/indexeddb.rs @@ -4,20 +4,20 @@ use std::sync::Mutex; use async_trait::async_trait; use chrono::{DateTime, Utc}; use js_sys::{Array, Map, Object, Reflect}; -use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; +use wasm_bindgen::prelude::*; use wasm_bindgen_futures::JsFuture; use web_sys::{ - IdbDatabase, IdbObjectStore, IdbOpenDbRequest, IdbRequest, - IdbTransaction, IdbTransactionMode, IdbVersionChangeEvent, + IdbDatabase, IdbObjectStore, IdbOpenDbRequest, IdbRequest, IdbTransaction, IdbTransactionMode, + IdbVersionChangeEvent, }; use crate::error::{AegisError, AegisResult}; use crate::storage::async_traits::{ AsyncStorageBackend, AsyncStorageTransaction, StorageCapabilities, }; -use crate::storage::traits::{IntegrityReport, StorageMeta, TupleFilter}; use crate::storage::traits::compute_event_hash; +use crate::storage::traits::{IntegrityReport, StorageMeta, TupleFilter}; use crate::types::{ AuditEntry, ConsistencyMode, PaginatedTuples, PaginationParams, PartitionId, Relation, RelationshipTuple, ResourceId, Revision, RevisionToken, SubjectId, TupleKey, TupleMutation, @@ -52,11 +52,15 @@ fn set_val(obj: &Object, key: &str, val: &JsValue) { } fn get_str(val: &JsValue, key: &str) -> Option { - Reflect::get(val, &JsValue::from_str(key)).ok().and_then(|v| v.as_string()) + Reflect::get(val, &JsValue::from_str(key)) + .ok() + .and_then(|v| v.as_string()) } fn get_num(val: &JsValue, key: &str) -> Option { - Reflect::get(val, &JsValue::from_str(key)).ok().and_then(|v| v.as_f64()) + Reflect::get(val, &JsValue::from_str(key)) + .ok() + .and_then(|v| v.as_f64()) } fn map_js(m: &HashMap) -> JsValue { @@ -69,7 +73,9 @@ fn map_js(m: &HashMap) -> JsValue { fn map_rust(val: &JsValue) -> HashMap { let mut m = HashMap::new(); - let Some(js_map) = val.dyn_ref::() else { return m }; + let Some(js_map) = val.dyn_ref::() else { + return m; + }; js_map.for_each(&mut |v, k| { if let (Some(kk), Some(vv)) = (k.as_string(), v.as_string()) { m.insert(kk, vv); @@ -127,10 +133,14 @@ fn js_to_tuple(val: &JsValue) -> AegisResult { Ok(t) } +#[allow(dead_code)] fn event_to_js(e: &AuditEntry, previous_hash: Option<&str>, event_hash: Option<&str>) -> JsValue { let obj = Object::new(); set_num(&obj, "revision", e.revision.as_u64() as f64); - let action = match e.action { TupleMutation::Add => "add", TupleMutation::Remove => "remove" }; + let action = match e.action { + TupleMutation::Add => "add", + TupleMutation::Remove => "remove", + }; set_str(&obj, "action", action); set_str(&obj, "subject", &e.subject); set_str(&obj, "relation", &e.relation); @@ -160,10 +170,14 @@ fn js_to_event(val: &JsValue) -> AegisResult { let subject = get_str(val, "subject").unwrap_or_default(); let relation = get_str(val, "relation").unwrap_or_default(); let object = get_str(val, "object").unwrap_or_default(); - let ts = get_str(val, "timestamp").unwrap_or_default() - .parse::>().unwrap_or_else(|_| Utc::now()); - let metadata = Reflect::get(val, &JsValue::from_str("metadata")).ok() - .map(|v| map_rust(&v)).filter(|m| !m.is_empty()); + let ts = get_str(val, "timestamp") + .unwrap_or_default() + .parse::>() + .unwrap_or_else(|_| Utc::now()); + let metadata = Reflect::get(val, &JsValue::from_str("metadata")) + .ok() + .map(|v| map_rust(&v)) + .filter(|m| !m.is_empty()); let identity = get_str(val, "identity"); Ok(AuditEntry { @@ -178,15 +192,22 @@ fn js_to_event(val: &JsValue) -> AegisResult { }) } +#[allow(dead_code)] fn js_event_hash(val: &JsValue) -> (Option, Option) { (get_str(val, "previous_hash"), get_str(val, "event_hash")) } fn event_obj_from_fields( - revision: f64, action: &str, - subject: &str, relation: &str, object: &str, - timestamp: &str, metadata: Option<&str>, identity: Option<&str>, - previous_hash: &str, event_hash: &str, + revision: f64, + action: &str, + subject: &str, + relation: &str, + object: &str, + timestamp: &str, + metadata: Option<&str>, + identity: Option<&str>, + previous_hash: &str, + event_hash: &str, ) -> JsValue { let obj = Object::new(); set_num(&obj, "revision", revision); @@ -195,18 +216,27 @@ fn event_obj_from_fields( set_str(&obj, "relation", relation); set_str(&obj, "object", object); set_str(&obj, "timestamp", timestamp); - if let Some(m) = metadata { set_str(&obj, "metadata", m); } - if let Some(id) = identity { set_str(&obj, "identity", id); } + if let Some(m) = metadata { + set_str(&obj, "metadata", m); + } + if let Some(id) = identity { + set_str(&obj, "identity", id); + } set_str(&obj, "previous_hash", previous_hash); set_str(&obj, "event_hash", event_hash); obj.into() } async fn last_event_hash_s(txn: &IdbTransaction, store_name: &str) -> AegisResult { - let store = txn.object_store(store_name) + let store = txn + .object_store(store_name) .map_err(|e| aegis_err(&format!("store {}: {:?}", store_name, e)))?; - let req = store.get_all().map_err(|e| aegis_err(&format!("get_all: {:?}", e)))?; - let val = req_future(req).await.map_err(|e| aegis_err(&format!("get_all rej: {:?}", e)))?; + let req = store + .get_all() + .map_err(|e| aegis_err(&format!("get_all: {:?}", e)))?; + let val = req_future(req) + .await + .map_err(|e| aegis_err(&format!("get_all rej: {:?}", e)))?; let arr: js_sys::Array = val.into(); let mut best_rev = -1.0; let mut best_hash = String::new(); @@ -228,13 +258,23 @@ fn req_future(req: IdbRequest) -> JsFuture { let success_req = req.clone(); let error_req = req.clone(); let onsuccess = Closure::once_into_js(move || { - resolve.call1(&JsValue::null(), &success_req.result().ok().unwrap_or(JsValue::null())).ok(); + resolve + .call1( + &JsValue::null(), + &success_req.result().ok().unwrap_or(JsValue::null()), + ) + .ok(); }); let onerror = Closure::once_into_js(move || { - let msg = error_req.error().ok().flatten() + let msg = error_req + .error() + .ok() + .flatten() .map(|d: web_sys::DomException| d.message()) .unwrap_or_else(|| "IndexedDB error".to_string()); - reject.call1(&JsValue::null(), &JsValue::from_str(&msg)).ok(); + reject + .call1(&JsValue::null(), &JsValue::from_str(&msg)) + .ok(); }); req.set_onsuccess(Some(onsuccess.unchecked_ref())); req.set_onerror(Some(onerror.unchecked_ref())); @@ -275,18 +315,29 @@ async fn open_db(name: &str, version: u32) -> AegisResult { open_req.set_onupgradeneeded(Some(upgrade.as_ref().unchecked_ref())); upgrade.forget(); - let val = req_future(open_req.into()).await + let val = req_future(open_req.into()) + .await .map_err(|e| aegis_err(&format!("open rejected: {:?}", e)))?; Ok(val.into()) } -fn store<'a>(db: &'a IdbDatabase, name: &str, mode: IdbTransactionMode) -> AegisResult { - let txn = db.transaction_with_str_and_mode(name, mode) +fn store<'a>( + db: &'a IdbDatabase, + name: &str, + mode: IdbTransactionMode, +) -> AegisResult { + let txn = db + .transaction_with_str_and_mode(name, mode) .map_err(|e| aegis_err(&format!("txn: {:?}", e)))?; - txn.object_store(name).map_err(|e| aegis_err(&format!("store {}: {:?}", name, e))) + txn.object_store(name) + .map_err(|e| aegis_err(&format!("store {}: {:?}", name, e))) } -fn multi_store_txn<'a>(db: &'a IdbDatabase, names: &[&str], mode: IdbTransactionMode) -> AegisResult { +fn multi_store_txn<'a>( + db: &'a IdbDatabase, + names: &[&str], + mode: IdbTransactionMode, +) -> AegisResult { let arr = js_sys::Array::new(); for name in names { arr.push(&JsValue::from_str(name)); @@ -295,58 +346,95 @@ fn multi_store_txn<'a>(db: &'a IdbDatabase, names: &[&str], mode: IdbTransaction .map_err(|e| aegis_err(&format!("txn: {:?}", e))) } -async fn put_s_in_txn(txn: &IdbTransaction, store_name: &str, key: &JsValue, val: &JsValue) -> AegisResult<()> { - let store = txn.object_store(store_name) +async fn put_s_in_txn( + txn: &IdbTransaction, + store_name: &str, + key: &JsValue, + val: &JsValue, +) -> AegisResult<()> { + let store = txn + .object_store(store_name) .map_err(|e| aegis_err(&format!("store {}: {:?}", store_name, e)))?; - let req = store.put_with_key(val, key) + let req = store + .put_with_key(val, key) .map_err(|e| aegis_err(&format!("put: {:?}", e)))?; - req_future(req).await.map_err(|e| aegis_err(&format!("put rej: {:?}", e)))?; + req_future(req) + .await + .map_err(|e| aegis_err(&format!("put rej: {:?}", e)))?; Ok(()) } async fn del_s_in_txn(txn: &IdbTransaction, store_name: &str, key: &JsValue) -> AegisResult<()> { - let store = txn.object_store(store_name) + let store = txn + .object_store(store_name) .map_err(|e| aegis_err(&format!("store {}: {:?}", store_name, e)))?; - let req = store.delete(key).map_err(|e| aegis_err(&format!("del: {:?}", e)))?; - req_future(req).await.map_err(|e| aegis_err(&format!("del rej: {:?}", e)))?; + let req = store + .delete(key) + .map_err(|e| aegis_err(&format!("del: {:?}", e)))?; + req_future(req) + .await + .map_err(|e| aegis_err(&format!("del rej: {:?}", e)))?; Ok(()) } async fn put_s(store: &IdbObjectStore, key: &JsValue, val: &JsValue) -> AegisResult<()> { - let req = store.put_with_key(val, key) + let req = store + .put_with_key(val, key) .map_err(|e| aegis_err(&format!("put: {:?}", e)))?; - req_future(req).await.map_err(|e| aegis_err(&format!("put rej: {:?}", e)))?; + req_future(req) + .await + .map_err(|e| aegis_err(&format!("put rej: {:?}", e)))?; Ok(()) } async fn get_s(store: &IdbObjectStore, key: &JsValue) -> AegisResult> { - let req = store.get(key).map_err(|e| aegis_err(&format!("get: {:?}", e)))?; - let val = req_future(req).await.map_err(|e| aegis_err(&format!("get rej: {:?}", e)))?; + let req = store + .get(key) + .map_err(|e| aegis_err(&format!("get: {:?}", e)))?; + let val = req_future(req) + .await + .map_err(|e| aegis_err(&format!("get rej: {:?}", e)))?; Ok((!val.is_null() && !val.is_undefined()).then_some(val)) } async fn del_s(store: &IdbObjectStore, key: &JsValue) -> AegisResult<()> { - let req = store.delete(key).map_err(|e| aegis_err(&format!("del: {:?}", e)))?; - req_future(req).await.map_err(|e| aegis_err(&format!("del rej: {:?}", e)))?; + let req = store + .delete(key) + .map_err(|e| aegis_err(&format!("del: {:?}", e)))?; + req_future(req) + .await + .map_err(|e| aegis_err(&format!("del rej: {:?}", e)))?; Ok(()) } async fn all_s(store: &IdbObjectStore) -> AegisResult> { - let req = store.get_all().map_err(|e| aegis_err(&format!("all: {:?}", e)))?; - let val = req_future(req).await.map_err(|e| aegis_err(&format!("all rej: {:?}", e)))?; + let req = store + .get_all() + .map_err(|e| aegis_err(&format!("all: {:?}", e)))?; + let val = req_future(req) + .await + .map_err(|e| aegis_err(&format!("all rej: {:?}", e)))?; let arr: Array = val.into(); let mut out = Vec::with_capacity(arr.length() as usize); - for i in 0..arr.length() { out.push(arr.get(i)); } + for i in 0..arr.length() { + out.push(arr.get(i)); + } Ok(out) } async fn all_keys_s(store: &IdbObjectStore) -> AegisResult> { - let req = store.get_all_keys().map_err(|e| aegis_err(&format!("keys: {:?}", e)))?; - let val = req_future(req).await.map_err(|e| aegis_err(&format!("keys rej: {:?}", e)))?; + let req = store + .get_all_keys() + .map_err(|e| aegis_err(&format!("keys: {:?}", e)))?; + let val = req_future(req) + .await + .map_err(|e| aegis_err(&format!("keys rej: {:?}", e)))?; let arr: Array = val.into(); let mut out = Vec::with_capacity(arr.length() as usize); for i in 0..arr.length() { - if let Some(s) = arr.get(i).as_string() { out.push(s); } + if let Some(s) = arr.get(i).as_string() { + out.push(s); + } } Ok(out) } @@ -360,7 +448,9 @@ pub struct IndexedDbStorage { } impl IndexedDbStorage { - pub fn new() -> Self { Self::with_name(DB_NAME) } + pub fn new() -> Self { + Self::with_name(DB_NAME) + } pub fn with_name(name: &str) -> Self { Self { @@ -373,7 +463,8 @@ impl IndexedDbStorage { } fn db(&self) -> AegisResult { - self.db.lock() + self.db + .lock() .map_err(|e| aegis_err(&format!("lock: {}", e)))? .clone() .ok_or_else(|| aegis_err("IndexedDB not initialized")) @@ -404,9 +495,18 @@ impl AsyncStorageBackend for IndexedDbStorage { } }; - *self.current_rev.lock().map_err(|e| aegis_err(&format!("lock: {}", e)))? = rev.as_u64(); - *self.db.lock().map_err(|e| aegis_err(&format!("lock: {}", e)))? = Some(db); - *self.schema_ver.lock().map_err(|e| aegis_err(&format!("lock: {}", e)))? = schema_ver; + *self + .current_rev + .lock() + .map_err(|e| aegis_err(&format!("lock: {}", e)))? = rev.as_u64(); + *self + .db + .lock() + .map_err(|e| aegis_err(&format!("lock: {}", e)))? = Some(db); + *self + .schema_ver + .lock() + .map_err(|e| aegis_err(&format!("lock: {}", e)))? = schema_ver; Ok(StorageMeta { schema_version: schema_ver, @@ -416,184 +516,389 @@ impl AsyncStorageBackend for IndexedDbStorage { }) } - async fn write_tuple(&self, pid: &PartitionId, tuple: &RelationshipTuple) -> AegisResult { + async fn write_tuple( + &self, + pid: &PartitionId, + tuple: &RelationshipTuple, + ) -> AegisResult { let db = self.db()?; - let txn = multi_store_txn(&db, &[STORE_TUPLES, STORE_EVENTS, STORE_REVISION], IdbTransactionMode::Readwrite)?; + let txn = multi_store_txn( + &db, + &[STORE_TUPLES, STORE_EVENTS, STORE_REVISION], + IdbTransactionMode::Readwrite, + )?; let cur = { - let mut rev = self.current_rev.lock().map_err(|e| aegis_err(&format!("lock: {}", e)))?; + let mut rev = self + .current_rev + .lock() + .map_err(|e| aegis_err(&format!("lock: {}", e)))?; *rev += 1; *rev }; let rev = Revision::new(cur); - put_s_in_txn(&txn, STORE_REVISION, &rev_key(), &JsValue::from_f64(rev.as_u64() as f64)).await?; - put_s_in_txn(&txn, STORE_TUPLES, &JsValue::from_str(&pkey(pid, tuple.subject.as_str(), tuple.relation.as_str(), tuple.object.as_str())), &tuple_to_js(tuple)).await?; + put_s_in_txn( + &txn, + STORE_REVISION, + &rev_key(), + &JsValue::from_f64(rev.as_u64() as f64), + ) + .await?; + put_s_in_txn( + &txn, + STORE_TUPLES, + &JsValue::from_str(&pkey( + pid, + tuple.subject.as_str(), + tuple.relation.as_str(), + tuple.object.as_str(), + )), + &tuple_to_js(tuple), + ) + .await?; let actor = self.actor.lock().ok().and_then(|g| g.clone()); let action = "add"; let now_rfc = Utc::now().to_rfc3339(); let last_hash = last_event_hash_s(&txn, STORE_EVENTS).await?; let event_hash = compute_event_hash( - &last_hash, rev.as_u64() as i64, action, - tuple.subject.as_str(), tuple.relation.as_str(), tuple.object.as_str(), - pid.as_str(), None, &now_rfc, actor.as_deref(), + &last_hash, + rev.as_u64() as i64, + action, + tuple.subject.as_str(), + tuple.relation.as_str(), + tuple.object.as_str(), + pid.as_str(), + None, + &now_rfc, + actor.as_deref(), ); let event_obj = event_obj_from_fields( - rev.as_u64() as f64, action, - tuple.subject.as_str(), tuple.relation.as_str(), tuple.object.as_str(), - &now_rfc, None, actor.as_deref(), - &last_hash, &event_hash, + rev.as_u64() as f64, + action, + tuple.subject.as_str(), + tuple.relation.as_str(), + tuple.object.as_str(), + &now_rfc, + None, + actor.as_deref(), + &last_hash, + &event_hash, ); - put_s_in_txn(&txn, STORE_EVENTS, &JsValue::from_str(&ekey(pid, rev)), &event_obj).await?; + put_s_in_txn( + &txn, + STORE_EVENTS, + &JsValue::from_str(&ekey(pid, rev)), + &event_obj, + ) + .await?; drop(txn); Ok(rev) } - async fn write_tuples_batch(&self, pid: &PartitionId, tuples: &[RelationshipTuple]) -> AegisResult { + async fn write_tuples_batch( + &self, + pid: &PartitionId, + tuples: &[RelationshipTuple], + ) -> AegisResult { let mut last = Revision::ZERO; - for t in tuples { last = self.write_tuple(pid, t).await?; } + for t in tuples { + last = self.write_tuple(pid, t).await?; + } Ok(last) } async fn delete_tuple(&self, pid: &PartitionId, key: &TupleKey) -> AegisResult { let db = self.db()?; - let txn = multi_store_txn(&db, &[STORE_TUPLES, STORE_EVENTS, STORE_REVISION], IdbTransactionMode::Readwrite)?; + let txn = multi_store_txn( + &db, + &[STORE_TUPLES, STORE_EVENTS, STORE_REVISION], + IdbTransactionMode::Readwrite, + )?; let cur = { - let mut rev = self.current_rev.lock().map_err(|e| aegis_err(&format!("lock: {}", e)))?; + let mut rev = self + .current_rev + .lock() + .map_err(|e| aegis_err(&format!("lock: {}", e)))?; *rev += 1; *rev }; let rev = Revision::new(cur); - put_s_in_txn(&txn, STORE_REVISION, &rev_key(), &JsValue::from_f64(rev.as_u64() as f64)).await?; - del_s_in_txn(&txn, STORE_TUPLES, &JsValue::from_str(&pkey(pid, key.subject.as_str(), key.relation.as_str(), key.object.as_str()))).await?; + put_s_in_txn( + &txn, + STORE_REVISION, + &rev_key(), + &JsValue::from_f64(rev.as_u64() as f64), + ) + .await?; + del_s_in_txn( + &txn, + STORE_TUPLES, + &JsValue::from_str(&pkey( + pid, + key.subject.as_str(), + key.relation.as_str(), + key.object.as_str(), + )), + ) + .await?; let actor = self.actor.lock().ok().and_then(|g| g.clone()); let action = "remove"; let now_rfc = Utc::now().to_rfc3339(); let last_hash = last_event_hash_s(&txn, STORE_EVENTS).await?; let event_hash = compute_event_hash( - &last_hash, rev.as_u64() as i64, action, - key.subject.as_str(), key.relation.as_str(), key.object.as_str(), - pid.as_str(), None, &now_rfc, actor.as_deref(), + &last_hash, + rev.as_u64() as i64, + action, + key.subject.as_str(), + key.relation.as_str(), + key.object.as_str(), + pid.as_str(), + None, + &now_rfc, + actor.as_deref(), ); let event_obj = event_obj_from_fields( - rev.as_u64() as f64, action, - key.subject.as_str(), key.relation.as_str(), key.object.as_str(), - &now_rfc, None, actor.as_deref(), - &last_hash, &event_hash, + rev.as_u64() as f64, + action, + key.subject.as_str(), + key.relation.as_str(), + key.object.as_str(), + &now_rfc, + None, + actor.as_deref(), + &last_hash, + &event_hash, ); - put_s_in_txn(&txn, STORE_EVENTS, &JsValue::from_str(&ekey(pid, rev)), &event_obj).await?; + put_s_in_txn( + &txn, + STORE_EVENTS, + &JsValue::from_str(&ekey(pid, rev)), + &event_obj, + ) + .await?; drop(txn); Ok(rev) } - async fn delete_subject(&self, pid: &PartitionId, subject: &SubjectId) -> AegisResult { + async fn delete_subject( + &self, + pid: &PartitionId, + subject: &SubjectId, + ) -> AegisResult { let prefix = format!("{}:{}:", pid.as_str(), subject.as_str()); let tuples = self.scan_prefix(pid, &prefix).await?; let mut last = Revision::ZERO; for t in tuples { - last = self.delete_tuple(pid, &TupleKey { subject: t.subject, relation: t.relation, object: t.object }).await?; + last = self + .delete_tuple( + pid, + &TupleKey { + subject: t.subject, + relation: t.relation, + object: t.object, + }, + ) + .await?; + } + if last == Revision::ZERO { + last = self.current_revision(pid).await?; } - if last == Revision::ZERO { last = self.current_revision(pid).await?; } Ok(last) } async fn delete_object(&self, pid: &PartitionId, object: &ResourceId) -> AegisResult { - let tuples = self.list_by_object(pid, object, None, &ConsistencyMode::MinimizeLatency).await?; + let tuples = self + .list_by_object(pid, object, None, &ConsistencyMode::MinimizeLatency) + .await?; let mut last = Revision::ZERO; for t in tuples { - last = self.delete_tuple(pid, &TupleKey { subject: t.subject, relation: t.relation, object: t.object }).await?; + last = self + .delete_tuple( + pid, + &TupleKey { + subject: t.subject, + relation: t.relation, + object: t.object, + }, + ) + .await?; + } + if last == Revision::ZERO { + last = self.current_revision(pid).await?; } - if last == Revision::ZERO { last = self.current_revision(pid).await?; } Ok(last) } async fn has_tuple(&self, pid: &PartitionId, key: &TupleKey) -> AegisResult { let s = store(&self.db()?, STORE_TUPLES, IdbTransactionMode::Readonly)?; - Ok(get_s(&s, &JsValue::from_str(&pkey(pid, key.subject.as_str(), key.relation.as_str(), key.object.as_str()))).await?.is_some()) - } - - async fn read_tuple(&self, pid: &PartitionId, key: &TupleKey) -> AegisResult> { + Ok(get_s( + &s, + &JsValue::from_str(&pkey( + pid, + key.subject.as_str(), + key.relation.as_str(), + key.object.as_str(), + )), + ) + .await? + .is_some()) + } + + async fn read_tuple( + &self, + pid: &PartitionId, + key: &TupleKey, + ) -> AegisResult> { let s = store(&self.db()?, STORE_TUPLES, IdbTransactionMode::Readonly)?; - match get_s(&s, &JsValue::from_str(&pkey(pid, key.subject.as_str(), key.relation.as_str(), key.object.as_str()))).await? { + match get_s( + &s, + &JsValue::from_str(&pkey( + pid, + key.subject.as_str(), + key.relation.as_str(), + key.object.as_str(), + )), + ) + .await? + { Some(v) => Ok(Some(js_to_tuple(&v)?)), None => Ok(None), } } - async fn list_by_object(&self, _pid: &PartitionId, object: &ResourceId, relation: Option<&Relation>, _consistency: &ConsistencyMode) -> AegisResult> { + async fn list_by_object( + &self, + _pid: &PartitionId, + object: &ResourceId, + relation: Option<&Relation>, + _consistency: &ConsistencyMode, + ) -> AegisResult> { let s = store(&self.db()?, STORE_TUPLES, IdbTransactionMode::Readonly)?; let all = all_s(&s).await?; let mut out = Vec::new(); for v in all { let t = js_to_tuple(&v)?; - if t.object == *object && relation.map_or(true, |r| t.relation == *r) { out.push(t); } + if t.object == *object && relation.map_or(true, |r| t.relation == *r) { + out.push(t); + } } Ok(out) } - async fn list_by_subject(&self, _pid: &PartitionId, subject: &SubjectId, relation: Option<&Relation>, _consistency: &ConsistencyMode) -> AegisResult> { + async fn list_by_subject( + &self, + _pid: &PartitionId, + subject: &SubjectId, + relation: Option<&Relation>, + _consistency: &ConsistencyMode, + ) -> AegisResult> { let s = store(&self.db()?, STORE_TUPLES, IdbTransactionMode::Readonly)?; let all = all_s(&s).await?; let mut out = Vec::new(); for v in all { let t = js_to_tuple(&v)?; - if t.subject == *subject && relation.map_or(true, |r| t.relation == *r) { out.push(t); } + if t.subject == *subject && relation.map_or(true, |r| t.relation == *r) { + out.push(t); + } } Ok(out) } - async fn list_by_relation(&self, _pid: &PartitionId, object: &ResourceId, relation: &Relation) -> AegisResult> { + async fn list_by_relation( + &self, + _pid: &PartitionId, + object: &ResourceId, + relation: &Relation, + ) -> AegisResult> { let s = store(&self.db()?, STORE_TUPLES, IdbTransactionMode::Readonly)?; let all = all_s(&s).await?; let mut out = Vec::new(); for v in all { let t = js_to_tuple(&v)?; - if t.object == *object && t.relation == *relation { out.push(t); } + if t.object == *object && t.relation == *relation { + out.push(t); + } } Ok(out) } - async fn query_tuples(&self, pid: &PartitionId, filter: &TupleFilter, pagination: &PaginationParams, _consistency: &ConsistencyMode) -> AegisResult { + async fn query_tuples( + &self, + pid: &PartitionId, + filter: &TupleFilter, + pagination: &PaginationParams, + _consistency: &ConsistencyMode, + ) -> AegisResult { let s = store(&self.db()?, STORE_TUPLES, IdbTransactionMode::Readonly)?; let all = all_s(&s).await?; let mut filtered: Vec = Vec::new(); for v in all { let t = js_to_tuple(&v)?; - if filter.subject_type.as_ref().map_or(true, |st| t.subject.as_str().starts_with(st.trim_end_matches('#'))) - && filter.relation.as_ref().map_or(true, |r| t.relation == *r) - && filter.object_type.as_ref().map_or(true, |ot| t.object.as_str().starts_with(ot)) - { filtered.push(t); } + if filter.subject_type.as_ref().map_or(true, |st| { + t.subject.as_str().starts_with(st.trim_end_matches('#')) + }) && filter.relation.as_ref().map_or(true, |r| t.relation == *r) + && filter + .object_type + .as_ref() + .map_or(true, |ot| t.object.as_str().starts_with(ot)) + { + filtered.push(t); + } } let total = filtered.len(); - let offset = pagination.cursor.as_ref().map(|c| c.offset as usize).unwrap_or(0); + let offset = pagination + .cursor + .as_ref() + .map(|c| c.offset as usize) + .unwrap_or(0); let limit = pagination.limit as usize; let has_more = offset + limit < total; filtered = filtered.into_iter().skip(offset).take(limit).collect(); let revision = self.current_revision(pid).await?; Ok(PaginatedTuples { tuples: filtered, - next_cursor: has_more.then_some(crate::types::PaginationCursor { offset: (offset + limit) as u64, revision }), + next_cursor: has_more.then_some(crate::types::PaginationCursor { + offset: (offset + limit) as u64, + revision, + }), revision, }) } async fn current_revision(&self, _pid: &PartitionId) -> AegisResult { - Ok(Revision::new(*self.current_rev.lock().map_err(|e| aegis_err(&format!("lock: {}", e)))?)) + Ok(Revision::new( + *self + .current_rev + .lock() + .map_err(|e| aegis_err(&format!("lock: {}", e)))?, + )) } async fn read_schema_version(&self) -> AegisResult { - Ok(*self.schema_ver.lock().map_err(|e| aegis_err(&format!("lock: {}", e)))?) + Ok(*self + .schema_ver + .lock() + .map_err(|e| aegis_err(&format!("lock: {}", e)))?) } async fn write_schema_version(&self, version: u32) -> AegisResult<()> { let s = store(&self.db()?, STORE_SCHEMA, IdbTransactionMode::Readwrite)?; - put_s(&s, &JsValue::from_str("version"), &JsValue::from_f64(version as f64)).await?; - *self.schema_ver.lock().map_err(|e| aegis_err(&format!("lock: {}", e)))? = version; + put_s( + &s, + &JsValue::from_str("version"), + &JsValue::from_f64(version as f64), + ) + .await?; + *self + .schema_ver + .lock() + .map_err(|e| aegis_err(&format!("lock: {}", e)))? = version; Ok(()) } @@ -602,9 +907,15 @@ impl AsyncStorageBackend for IndexedDbStorage { Ok(RevisionToken::new(rev, uuid::Uuid::new_v4())) } - async fn begin_transaction(&self, _pid: &PartitionId) -> AegisResult> { + async fn begin_transaction( + &self, + _pid: &PartitionId, + ) -> AegisResult> { let db = self.db()?; - let rev = *self.current_rev.lock().map_err(|e| aegis_err(&format!("lock: {}", e)))?; + let rev = *self + .current_rev + .lock() + .map_err(|e| aegis_err(&format!("lock: {}", e)))?; let actor = self.actor.lock().ok().and_then(|g| g.clone()); Ok(Box::new(IndexedDbTransaction { db, @@ -614,7 +925,14 @@ impl AsyncStorageBackend for IndexedDbStorage { })) } - async fn query_audit(&self, _pid: &PartitionId, object: Option<&ResourceId>, from: Option, to: Option, _p: &PaginationParams) -> AegisResult> { + async fn query_audit( + &self, + _pid: &PartitionId, + object: Option<&ResourceId>, + from: Option, + to: Option, + _p: &PaginationParams, + ) -> AegisResult> { let s = store(&self.db()?, STORE_EVENTS, IdbTransactionMode::Readonly)?; let all = all_s(&s).await?; let mut out: Vec = Vec::new(); @@ -623,7 +941,9 @@ impl AsyncStorageBackend for IndexedDbStorage { if object.map_or(true, |o| e.object == o.as_str()) && from.map_or(true, |f| e.revision >= f) && to.map_or(true, |t| e.revision <= t) - { out.push(e); } + { + out.push(e); + } } out.sort_by_key(|e| e.revision); Ok(out) @@ -640,7 +960,11 @@ impl AsyncStorageBackend for IndexedDbStorage { }) } - async fn delete_events_before(&self, pid: &PartitionId, cutoff: DateTime) -> AegisResult { + async fn delete_events_before( + &self, + pid: &PartitionId, + cutoff: DateTime, + ) -> AegisResult { let s = store(&self.db()?, STORE_EVENTS, IdbTransactionMode::Readwrite)?; let all = all_s(&s).await?; let mut n = 0; @@ -654,15 +978,33 @@ impl AsyncStorageBackend for IndexedDbStorage { Ok(n) } - async fn compact_events(&self, _pid: &PartitionId) -> AegisResult { Ok(0) } - async fn delete_soft_deleted_tuples_before(&self, _pid: &PartitionId, _cutoff: DateTime) -> AegisResult { Ok(0) } + async fn compact_events(&self, _pid: &PartitionId) -> AegisResult { + Ok(0) + } + async fn delete_soft_deleted_tuples_before( + &self, + _pid: &PartitionId, + _cutoff: DateTime, + ) -> AegisResult { + Ok(0) + } - async fn recover_from_events(&self, pid: &PartitionId, to_rev: Option) -> AegisResult { - let events = self.query_audit(pid, None, None, to_rev, &PaginationParams::default()).await?; + async fn recover_from_events( + &self, + pid: &PartitionId, + to_rev: Option, + ) -> AegisResult { + let events = self + .query_audit(pid, None, None, to_rev, &PaginationParams::default()) + .await?; let s = store(&self.db()?, STORE_TUPLES, IdbTransactionMode::Readwrite)?; let all = all_keys_s(&s).await?; let prefix = format!("{}:", pid.as_str()); - for k in all { if k.starts_with(&prefix) { del_s(&s, &JsValue::from_str(&k)).await?; } } + for k in all { + if k.starts_with(&prefix) { + del_s(&s, &JsValue::from_str(&k)).await?; + } + } let mut last = Revision::ZERO; for e in &events { match e.action { @@ -672,10 +1014,19 @@ impl AsyncStorageBackend for IndexedDbStorage { Relation::new(&e.relation).map_err(|e| AegisError::Validation(e))?, ResourceId::new(&e.object).map_err(|e| AegisError::Validation(e))?, ); - put_s(&s, &JsValue::from_str(&pkey(pid, &e.subject, &e.relation, &e.object)), &tuple_to_js(&t)).await?; + put_s( + &s, + &JsValue::from_str(&pkey(pid, &e.subject, &e.relation, &e.object)), + &tuple_to_js(&t), + ) + .await?; } TupleMutation::Remove => { - del_s(&s, &JsValue::from_str(&pkey(pid, &e.subject, &e.relation, &e.object))).await?; + del_s( + &s, + &JsValue::from_str(&pkey(pid, &e.subject, &e.relation, &e.object)), + ) + .await?; } } last = e.revision; @@ -683,31 +1034,70 @@ impl AsyncStorageBackend for IndexedDbStorage { Ok(last) } - async fn restore_backup(&self, pid: &PartitionId, tuples: &[RelationshipTuple], events: &[AuditEntry], revision: Revision) -> AegisResult<()> { + async fn restore_backup( + &self, + pid: &PartitionId, + tuples: &[RelationshipTuple], + events: &[AuditEntry], + revision: Revision, + ) -> AegisResult<()> { let _ = self.recover_from_events(pid, None).await?; let s = store(&self.db()?, STORE_TUPLES, IdbTransactionMode::Readwrite)?; - for t in tuples { put_s(&s, &JsValue::from_str(&pkey(pid, t.subject.as_str(), t.relation.as_str(), t.object.as_str())), &tuple_to_js(t)).await?; } + for t in tuples { + put_s( + &s, + &JsValue::from_str(&pkey( + pid, + t.subject.as_str(), + t.relation.as_str(), + t.object.as_str(), + )), + &tuple_to_js(t), + ) + .await?; + } let se = store(&self.db()?, STORE_EVENTS, IdbTransactionMode::Readwrite)?; let mut last_hash = String::new(); for e in events { - let action_str = match e.action { TupleMutation::Add => "add", TupleMutation::Remove => "remove" }; + let action_str = match e.action { + TupleMutation::Add => "add", + TupleMutation::Remove => "remove", + }; let ts = e.timestamp.to_rfc3339(); let event_hash = compute_event_hash( - &last_hash, e.revision.as_u64() as i64, action_str, - &e.subject, &e.relation, &e.object, - pid.as_str(), None, &ts, e.identity.as_deref(), + &last_hash, + e.revision.as_u64() as i64, + action_str, + &e.subject, + &e.relation, + &e.object, + pid.as_str(), + None, + &ts, + e.identity.as_deref(), ); let event_obj = event_obj_from_fields( - e.revision.as_u64() as f64, action_str, - &e.subject, &e.relation, &e.object, - &ts, None, e.identity.as_deref(), - &last_hash, &event_hash, + e.revision.as_u64() as f64, + action_str, + &e.subject, + &e.relation, + &e.object, + &ts, + None, + e.identity.as_deref(), + &last_hash, + &event_hash, ); put_s(&se, &JsValue::from_str(&ekey(pid, e.revision)), &event_obj).await?; last_hash = event_hash; } let sr = store(&self.db()?, STORE_REVISION, IdbTransactionMode::Readwrite)?; - put_s(&sr, &rev_key(), &JsValue::from_f64(revision.as_u64() as f64)).await?; + put_s( + &sr, + &rev_key(), + &JsValue::from_f64(revision.as_u64() as f64), + ) + .await?; Ok(()) } @@ -719,22 +1109,45 @@ impl AsyncStorageBackend for IndexedDbStorage { } async fn close(&self) -> AegisResult<()> { - if let Ok(mut g) = self.db.lock() { *g = None; } + if let Ok(mut g) = self.db.lock() { + *g = None; + } Ok(()) } - async fn save_policy_draft(&self, draft: &crate::engine::policy_lifecycle::PolicyDraft) -> AegisResult<()> { - let s = store(&self.db()?, STORE_POLICY_DRAFTS, IdbTransactionMode::Readwrite)?; - let json = serde_json::to_string(draft).map_err(|e| aegis_err(&format!("serialize: {}", e)))?; - put_s(&s, &JsValue::from_str(&draft.id.to_string()), &JsValue::from_str(&json)).await - } - - async fn load_policy_draft(&self, id: &str) -> AegisResult> { - let s = store(&self.db()?, STORE_POLICY_DRAFTS, IdbTransactionMode::Readonly)?; + async fn save_policy_draft( + &self, + draft: &crate::engine::policy_lifecycle::PolicyDraft, + ) -> AegisResult<()> { + let s = store( + &self.db()?, + STORE_POLICY_DRAFTS, + IdbTransactionMode::Readwrite, + )?; + let json = + serde_json::to_string(draft).map_err(|e| aegis_err(&format!("serialize: {}", e)))?; + put_s( + &s, + &JsValue::from_str(&draft.id.to_string()), + &JsValue::from_str(&json), + ) + .await + } + + async fn load_policy_draft( + &self, + id: &str, + ) -> AegisResult> { + let s = store( + &self.db()?, + STORE_POLICY_DRAFTS, + IdbTransactionMode::Readonly, + )?; match get_s(&s, &JsValue::from_str(id)).await? { Some(v) => { let json = v.as_string().ok_or_else(|| aegis_err("expected string"))?; - let draft = serde_json::from_str(&json).map_err(|e| aegis_err(&format!("deserialize: {}", e)))?; + let draft = serde_json::from_str(&json) + .map_err(|e| aegis_err(&format!("deserialize: {}", e)))?; Ok(Some(draft)) } None => Ok(None), @@ -742,7 +1155,11 @@ impl AsyncStorageBackend for IndexedDbStorage { } async fn delete_policy_draft(&self, id: &str) -> AegisResult { - let s = store(&self.db()?, STORE_POLICY_DRAFTS, IdbTransactionMode::Readwrite)?; + let s = store( + &self.db()?, + STORE_POLICY_DRAFTS, + IdbTransactionMode::Readwrite, + )?; let exists = get_s(&s, &JsValue::from_str(id)).await?.is_some(); if exists { del_s(&s, &JsValue::from_str(id)).await?; @@ -752,14 +1169,31 @@ impl AsyncStorageBackend for IndexedDbStorage { } } - async fn save_analysis_schedule(&self, schedule: &crate::engine::scheduler::AnalysisSchedule) -> AegisResult<()> { - let s = store(&self.db()?, STORE_ANALYSIS_SCHEDULES, IdbTransactionMode::Readwrite)?; - let json = serde_json::to_string(schedule).map_err(|e| aegis_err(&format!("serialize: {}", e)))?; - put_s(&s, &JsValue::from_str(&schedule.id.to_string()), &JsValue::from_str(&json)).await + async fn save_analysis_schedule( + &self, + schedule: &crate::engine::scheduler::AnalysisSchedule, + ) -> AegisResult<()> { + let s = store( + &self.db()?, + STORE_ANALYSIS_SCHEDULES, + IdbTransactionMode::Readwrite, + )?; + let json = + serde_json::to_string(schedule).map_err(|e| aegis_err(&format!("serialize: {}", e)))?; + put_s( + &s, + &JsValue::from_str(&schedule.id.to_string()), + &JsValue::from_str(&json), + ) + .await } async fn delete_analysis_schedule(&self, id: &str) -> AegisResult { - let s = store(&self.db()?, STORE_ANALYSIS_SCHEDULES, IdbTransactionMode::Readwrite)?; + let s = store( + &self.db()?, + STORE_ANALYSIS_SCHEDULES, + IdbTransactionMode::Readwrite, + )?; let exists = get_s(&s, &JsValue::from_str(id)).await?.is_some(); if exists { del_s(&s, &JsValue::from_str(id)).await?; @@ -769,16 +1203,42 @@ impl AsyncStorageBackend for IndexedDbStorage { } } - async fn save_analysis_run(&self, run: &crate::engine::scheduler::AnalysisRun) -> AegisResult<()> { - let s = store(&self.db()?, STORE_ANALYSIS_RUNS, IdbTransactionMode::Readwrite)?; - let json = serde_json::to_string(run).map_err(|e| aegis_err(&format!("serialize: {}", e)))?; - put_s(&s, &JsValue::from_str(&run.id.to_string()), &JsValue::from_str(&json)).await - } - - async fn save_enforcement_event(&self, event: &crate::engine::enforcement_history::EnforcementEvent) -> AegisResult<()> { - let s = store(&self.db()?, STORE_ENFORCEMENT_EVENTS, IdbTransactionMode::Readwrite)?; - let json = serde_json::to_string(event).map_err(|e| aegis_err(&format!("serialize: {}", e)))?; - put_s(&s, &JsValue::from_str(&event.id.to_string()), &JsValue::from_str(&json)).await + async fn save_analysis_run( + &self, + run: &crate::engine::scheduler::AnalysisRun, + ) -> AegisResult<()> { + let s = store( + &self.db()?, + STORE_ANALYSIS_RUNS, + IdbTransactionMode::Readwrite, + )?; + let json = + serde_json::to_string(run).map_err(|e| aegis_err(&format!("serialize: {}", e)))?; + put_s( + &s, + &JsValue::from_str(&run.id.to_string()), + &JsValue::from_str(&json), + ) + .await + } + + async fn save_enforcement_event( + &self, + event: &crate::engine::enforcement_history::EnforcementEvent, + ) -> AegisResult<()> { + let s = store( + &self.db()?, + STORE_ENFORCEMENT_EVENTS, + IdbTransactionMode::Readwrite, + )?; + let json = + serde_json::to_string(event).map_err(|e| aegis_err(&format!("serialize: {}", e)))?; + put_s( + &s, + &JsValue::from_str(&event.id.to_string()), + &JsValue::from_str(&json), + ) + .await } async fn verify_audit_chain(&self, pid: &PartitionId) -> AegisResult> { @@ -801,7 +1261,8 @@ impl AsyncStorageBackend for IndexedDbStorage { let revision = get_num(val, "revision").unwrap_or(0.0) as i64; let timestamp = get_str(val, "timestamp").unwrap_or_default(); let identity = get_str(val, "identity"); - let metadata = Reflect::get(val, &JsValue::from_str("metadata")).ok() + let metadata = Reflect::get(val, &JsValue::from_str("metadata")) + .ok() .and_then(|v| v.as_string()); let stored_prev_hash = get_str(val, "previous_hash").unwrap_or_default(); let stored_event_hash = get_str(val, "event_hash").unwrap_or_default(); @@ -814,8 +1275,16 @@ impl AsyncStorageBackend for IndexedDbStorage { } let expected = compute_event_hash( - &last_event_hash, revision, &action, &subject, &relation, &object, - pid.as_str(), metadata.as_deref(), ×tamp, identity.as_deref(), + &last_event_hash, + revision, + &action, + &subject, + &relation, + &object, + pid.as_str(), + metadata.as_deref(), + ×tamp, + identity.as_deref(), ); if !stored_event_hash.is_empty() && expected != stored_event_hash { @@ -836,14 +1305,25 @@ impl AsyncStorageBackend for IndexedDbStorage { } impl IndexedDbStorage { - async fn scan_prefix(&self, pid: &PartitionId, prefix: &str) -> AegisResult> { + async fn scan_prefix( + &self, + pid: &PartitionId, + prefix: &str, + ) -> AegisResult> { let s = store(&self.db()?, STORE_TUPLES, IdbTransactionMode::Readonly)?; let all = all_s(&s).await?; let mut out = Vec::new(); for v in all { let t = js_to_tuple(&v)?; - let k = pkey(pid, t.subject.as_str(), t.relation.as_str(), t.object.as_str()); - if k.starts_with(prefix) { out.push(t); } + let k = pkey( + pid, + t.subject.as_str(), + t.relation.as_str(), + t.object.as_str(), + ); + if k.starts_with(prefix) { + out.push(t); + } } Ok(out) } @@ -858,20 +1338,36 @@ struct IndexedDbTransaction { #[async_trait(?Send)] impl AsyncStorageTransaction for IndexedDbTransaction { - async fn write(&mut self, partition_id: &PartitionId, tuple: &RelationshipTuple) -> AegisResult<()> { - self.pending.push((partition_id.clone(), TupleMutation::Add, tuple.clone())); + async fn write( + &mut self, + partition_id: &PartitionId, + tuple: &RelationshipTuple, + ) -> AegisResult<()> { + self.pending + .push((partition_id.clone(), TupleMutation::Add, tuple.clone())); Ok(()) } async fn delete(&mut self, partition_id: &PartitionId, key: &TupleKey) -> AegisResult<()> { - let tuple = RelationshipTuple::new(key.subject.clone(), key.relation.clone(), key.object.clone()); - self.pending.push((partition_id.clone(), TupleMutation::Remove, tuple)); + let tuple = RelationshipTuple::new( + key.subject.clone(), + key.relation.clone(), + key.object.clone(), + ); + self.pending + .push((partition_id.clone(), TupleMutation::Remove, tuple)); Ok(()) } - async fn savepoint(&self, _name: &str) -> AegisResult<()> { Ok(()) } - async fn rollback_to_savepoint(&self, _name: &str) -> AegisResult<()> { Ok(()) } - async fn release_savepoint(&self, _name: &str) -> AegisResult<()> { Ok(()) } + async fn savepoint(&self, _name: &str) -> AegisResult<()> { + Ok(()) + } + async fn rollback_to_savepoint(&self, _name: &str) -> AegisResult<()> { + Ok(()) + } + async fn release_savepoint(&self, _name: &str) -> AegisResult<()> { + Ok(()) + } async fn set_actor_identity(&mut self, identity: Option) -> Option { let prev = self.actor.clone(); @@ -880,18 +1376,39 @@ impl AsyncStorageTransaction for IndexedDbTransaction { } async fn commit(self: Box) -> AegisResult { - let txn = multi_store_txn(&self.db, &[STORE_TUPLES, STORE_EVENTS, STORE_REVISION], IdbTransactionMode::Readwrite)?; + let txn = multi_store_txn( + &self.db, + &[STORE_TUPLES, STORE_EVENTS, STORE_REVISION], + IdbTransactionMode::Readwrite, + )?; let mut rev = self.rev; let mut last_hash = last_event_hash_s(&txn, STORE_EVENTS).await?; for (pid, action, tuple) in &self.pending { rev += 1; - put_s_in_txn(&txn, STORE_REVISION, &rev_key(), &JsValue::from_f64(rev as f64)).await?; - - let key = pkey(pid, tuple.subject.as_str(), tuple.relation.as_str(), tuple.object.as_str()); + put_s_in_txn( + &txn, + STORE_REVISION, + &rev_key(), + &JsValue::from_f64(rev as f64), + ) + .await?; + + let key = pkey( + pid, + tuple.subject.as_str(), + tuple.relation.as_str(), + tuple.object.as_str(), + ); match action { TupleMutation::Add => { - put_s_in_txn(&txn, STORE_TUPLES, &JsValue::from_str(&key), &tuple_to_js(tuple)).await?; + put_s_in_txn( + &txn, + STORE_TUPLES, + &JsValue::from_str(&key), + &tuple_to_js(tuple), + ) + .await?; } TupleMutation::Remove => { del_s_in_txn(&txn, STORE_TUPLES, &JsValue::from_str(&key)).await?; @@ -899,27 +1416,53 @@ impl AsyncStorageTransaction for IndexedDbTransaction { } let ekey_val = ekey(pid, Revision::new(rev)); - let action_str = match action { TupleMutation::Add => "add", TupleMutation::Remove => "remove" }; + let action_str = match action { + TupleMutation::Add => "add", + TupleMutation::Remove => "remove", + }; let now_rfc = Utc::now().to_rfc3339(); let actor = self.actor.clone(); let event_hash = compute_event_hash( - &last_hash, rev as i64, action_str, - tuple.subject.as_str(), tuple.relation.as_str(), tuple.object.as_str(), - pid.as_str(), None, &now_rfc, actor.as_deref(), + &last_hash, + rev as i64, + action_str, + tuple.subject.as_str(), + tuple.relation.as_str(), + tuple.object.as_str(), + pid.as_str(), + None, + &now_rfc, + actor.as_deref(), ); let event_obj = event_obj_from_fields( - rev as f64, action_str, - tuple.subject.as_str(), tuple.relation.as_str(), tuple.object.as_str(), - &now_rfc, None, actor.as_deref(), - &last_hash, &event_hash, + rev as f64, + action_str, + tuple.subject.as_str(), + tuple.relation.as_str(), + tuple.object.as_str(), + &now_rfc, + None, + actor.as_deref(), + &last_hash, + &event_hash, ); - put_s_in_txn(&txn, STORE_EVENTS, &JsValue::from_str(&ekey_val), &event_obj).await?; + put_s_in_txn( + &txn, + STORE_EVENTS, + &JsValue::from_str(&ekey_val), + &event_obj, + ) + .await?; last_hash = event_hash; } drop(txn); - if rev > self.rev { Ok(Revision::new(rev)) } else { Ok(Revision::ZERO) } + if rev > self.rev { + Ok(Revision::new(rev)) + } else { + Ok(Revision::ZERO) + } } async fn rollback(self: Box) -> AegisResult<()> { @@ -944,15 +1487,18 @@ mod wasm_tests { #[wasm_bindgen_test] async fn test_initialize_creates_stores() { let (storage, _pid) = setup().await; - let meta = storage.db().and_then(|db| { - let _ = db; - Ok(crate::storage::traits::StorageMeta { - schema_version: 1, - current_revision: Revision::ZERO, - backend_type: crate::storage::traits::BackendType::IndexedDB, - healthy: true, + let meta = storage + .db() + .and_then(|db| { + let _ = db; + Ok(crate::storage::traits::StorageMeta { + schema_version: 1, + current_revision: Revision::ZERO, + backend_type: crate::storage::traits::BackendType::IndexedDB, + healthy: true, + }) }) - }).unwrap(); + .unwrap(); assert!(meta.healthy); assert_eq!(meta.schema_version, 1); } @@ -1017,7 +1563,12 @@ mod wasm_tests { storage.write_tuple(&pid, &t2).await.unwrap(); let results = storage - .list_by_object(&pid, &ResourceId::new("repo:x").unwrap(), None, &ConsistencyMode::MinimizeLatency) + .list_by_object( + &pid, + &ResourceId::new("repo:x").unwrap(), + None, + &ConsistencyMode::MinimizeLatency, + ) .await .unwrap(); assert_eq!(results.len(), 2); @@ -1084,7 +1635,12 @@ mod wasm_tests { .unwrap(); let results = storage - .list_by_object(&pid, &ResourceId::new("repo:del").unwrap(), None, &ConsistencyMode::MinimizeLatency) + .list_by_object( + &pid, + &ResourceId::new("repo:del").unwrap(), + None, + &ConsistencyMode::MinimizeLatency, + ) .await .unwrap(); assert_eq!(results.len(), 0); @@ -1187,13 +1743,21 @@ mod wasm_tests { let avg = elapsed / 100.0; let p95 = avg; // simplified for inline test - web_sys::console::log_1(&format!( - "BENCH check_latency: avg={:.3}ms p95={:.3}ms (target <5ms) {}", - avg, p95, - if p95 < 5.0 { "PASS" } else { "FAIL" } - ).into()); + web_sys::console::log_1( + &format!( + "BENCH check_latency: avg={:.3}ms p95={:.3}ms (target <5ms) {}", + avg, + p95, + if p95 < 5.0 { "PASS" } else { "FAIL" } + ) + .into(), + ); - assert!(p95 < 50.0, "p95 check latency ({:.3}ms) exceeds 50ms threshold", p95); + assert!( + p95 < 50.0, + "p95 check latency ({:.3}ms) exceeds 50ms threshold", + p95 + ); } #[wasm_bindgen_test] @@ -1213,10 +1777,13 @@ mod wasm_tests { let elapsed = now_ms() - start; let throughput = (n as f64) / (elapsed / 1000.0); - web_sys::console::log_1(&format!( - "BENCH write_throughput: {} writes in {:.0}ms = {:.0} writes/sec", - n, elapsed, throughput - ).into()); + web_sys::console::log_1( + &format!( + "BENCH write_throughput: {} writes in {:.0}ms = {:.0} writes/sec", + n, elapsed, throughput + ) + .into(), + ); } #[wasm_bindgen_test] @@ -1227,17 +1794,21 @@ mod wasm_tests { let start = now_ms(); for _ in 0..50 { let _ = storage - .list_by_object(&pid, &ResourceId::new("doc:report").unwrap(), None, &ConsistencyMode::MinimizeLatency) + .list_by_object( + &pid, + &ResourceId::new("doc:report").unwrap(), + None, + &ConsistencyMode::MinimizeLatency, + ) .await .unwrap(); } let elapsed = now_ms() - start; let avg = elapsed / 50.0; - web_sys::console::log_1(&format!( - "BENCH list_by_object: avg={:.3}ms (1000 tuples)", - avg - ).into()); + web_sys::console::log_1( + &format!("BENCH list_by_object: avg={:.3}ms (1000 tuples)", avg).into(), + ); } #[wasm_bindgen_test] @@ -1280,7 +1851,10 @@ mod wasm_tests { assert!(rev > Revision::ZERO, "revision must survive page reload"); // Clean up - storage.delete_object(&pid, &ResourceId::new("doc:persist").unwrap()).await.unwrap(); + storage + .delete_object(&pid, &ResourceId::new("doc:persist").unwrap()) + .await + .unwrap(); } } @@ -1307,31 +1881,64 @@ mod wasm_tests { // Export let all = storage - .query_tuples(&pid, &TupleFilter::default(), &PaginationParams { cursor: None, limit: 100 }, &ConsistencyMode::MinimizeLatency) + .query_tuples( + &pid, + &TupleFilter::default(), + &PaginationParams { + cursor: None, + limit: 100, + }, + &ConsistencyMode::MinimizeLatency, + ) .await .unwrap(); let exported = all.tuples; // Delete all then verify empty for t in &exported { - storage.delete_tuple(&pid, &TupleKey { - subject: t.subject.clone(), - relation: t.relation.clone(), - object: t.object.clone(), - }).await.unwrap(); + storage + .delete_tuple( + &pid, + &TupleKey { + subject: t.subject.clone(), + relation: t.relation.clone(), + object: t.object.clone(), + }, + ) + .await + .unwrap(); } let after_del = storage - .query_tuples(&pid, &TupleFilter::default(), &PaginationParams { cursor: None, limit: 100 }, &ConsistencyMode::MinimizeLatency) + .query_tuples( + &pid, + &TupleFilter::default(), + &PaginationParams { + cursor: None, + limit: 100, + }, + &ConsistencyMode::MinimizeLatency, + ) .await .unwrap(); - assert!(after_del.tuples.is_empty(), "all tuples should be deleted before import"); + assert!( + after_del.tuples.is_empty(), + "all tuples should be deleted before import" + ); // Re-import via write_tuples_batch storage.write_tuples_batch(&pid, &exported).await.unwrap(); // Verify let after_import = storage - .query_tuples(&pid, &TupleFilter::default(), &PaginationParams { cursor: None, limit: 100 }, &ConsistencyMode::MinimizeLatency) + .query_tuples( + &pid, + &TupleFilter::default(), + &PaginationParams { + cursor: None, + limit: 100, + }, + &ConsistencyMode::MinimizeLatency, + ) .await .unwrap(); assert_eq!( @@ -1341,8 +1948,11 @@ mod wasm_tests { ); for t in &after_import.tuples { assert!( - exported.iter().any(|e| e.subject == t.subject && e.relation == t.relation && e.object == t.object), - "imported tuple must match exported: {:?}", t + exported.iter().any(|e| e.subject == t.subject + && e.relation == t.relation + && e.object == t.object), + "imported tuple must match exported: {:?}", + t ); } } @@ -1366,7 +1976,15 @@ mod wasm_tests { let start = now_ms(); let _all = storage - .query_tuples(&pid, &TupleFilter::default(), &PaginationParams { cursor: None, limit: n as u64 }, &ConsistencyMode::MinimizeLatency) + .query_tuples( + &pid, + &TupleFilter::default(), + &PaginationParams { + cursor: None, + limit: n as u64, + }, + &ConsistencyMode::MinimizeLatency, + ) .await .unwrap(); let elapsed = now_ms() - start; diff --git a/crates/aegis-core/src/storage/memory.rs b/crates/aegis-core/src/storage/memory.rs index 56e77c4..44f6a61 100644 --- a/crates/aegis-core/src/storage/memory.rs +++ b/crates/aegis-core/src/storage/memory.rs @@ -13,8 +13,8 @@ use crate::storage::traits::{ TupleFilter, }; use crate::types::{ - AuditEntry, PaginatedTuples, PaginationParams, PartitionId, Relation, - RelationshipTuple, ResourceId, Revision, RevisionToken, SubjectId, TupleKey, TupleMutation, + AuditEntry, PaginatedTuples, PaginationParams, PartitionId, Relation, RelationshipTuple, + ResourceId, Revision, RevisionToken, SubjectId, TupleKey, TupleMutation, }; type TupleMap = HashMap<(String, String, String), RelationshipTuple>; @@ -37,6 +37,12 @@ pub struct InMemoryStorage { inner: Arc>, } +impl Default for InMemoryStorage { + fn default() -> Self { + Self::new() + } +} + impl InMemoryStorage { pub fn new() -> Self { Self { @@ -61,7 +67,14 @@ impl InMemoryStorage { Revision::new(inner.revision) } - fn append_event(inner: &mut Inner, action: TupleMutation, subject: &str, relation: &str, object: &str, revision: Revision) { + fn append_event( + inner: &mut Inner, + action: TupleMutation, + subject: &str, + relation: &str, + object: &str, + revision: Revision, + ) { let identity = inner.actor_identity.clone(); let event = AuditEntry { revision, @@ -79,7 +92,10 @@ impl InMemoryStorage { impl StorageBackend for InMemoryStorage { fn initialize(&mut self) -> AegisResult { - let mut inner = self.inner.lock().map_err(|e| AegisError::Internal(e.to_string()))?; + let mut inner = self + .inner + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; inner.revision = 0; inner.schema_version = 1; inner.tuples.clear(); @@ -92,8 +108,15 @@ impl StorageBackend for InMemoryStorage { }) } - fn write_tuple(&self, _partition_id: &PartitionId, tuple: &RelationshipTuple) -> AegisResult { - let mut inner = self.inner.lock().map_err(|e| AegisError::Internal(e.to_string()))?; + fn write_tuple( + &self, + _partition_id: &PartitionId, + tuple: &RelationshipTuple, + ) -> AegisResult { + let mut inner = self + .inner + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; let key = ( tuple.subject.as_str().to_string(), tuple.relation.as_str().to_string(), @@ -101,12 +124,26 @@ impl StorageBackend for InMemoryStorage { ); let revision = Self::bump_revision(&mut inner); inner.tuples.insert(key, tuple.clone()); - Self::append_event(&mut inner, TupleMutation::Add, tuple.subject.as_str(), tuple.relation.as_str(), tuple.object.as_str(), revision); + Self::append_event( + &mut inner, + TupleMutation::Add, + tuple.subject.as_str(), + tuple.relation.as_str(), + tuple.object.as_str(), + revision, + ); Ok(revision) } - fn write_tuples_batch(&self, _partition_id: &PartitionId, tuples: &[RelationshipTuple]) -> AegisResult { - let mut inner = self.inner.lock().map_err(|e| AegisError::Internal(e.to_string()))?; + fn write_tuples_batch( + &self, + _partition_id: &PartitionId, + tuples: &[RelationshipTuple], + ) -> AegisResult { + let mut inner = self + .inner + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; let mut revision = Revision::ZERO; for tuple in tuples { let key = ( @@ -116,13 +153,23 @@ impl StorageBackend for InMemoryStorage { ); revision = Self::bump_revision(&mut inner); inner.tuples.insert(key, tuple.clone()); - Self::append_event(&mut inner, TupleMutation::Add, tuple.subject.as_str(), tuple.relation.as_str(), tuple.object.as_str(), revision); + Self::append_event( + &mut inner, + TupleMutation::Add, + tuple.subject.as_str(), + tuple.relation.as_str(), + tuple.object.as_str(), + revision, + ); } Ok(revision) } fn delete_tuple(&self, _partition_id: &PartitionId, key: &TupleKey) -> AegisResult { - let mut inner = self.inner.lock().map_err(|e| AegisError::Internal(e.to_string()))?; + let mut inner = self + .inner + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; let k = ( key.subject.as_str().to_string(), key.relation.as_str().to_string(), @@ -130,13 +177,29 @@ impl StorageBackend for InMemoryStorage { ); let revision = Self::bump_revision(&mut inner); inner.tuples.remove(&k); - Self::append_event(&mut inner, TupleMutation::Remove, key.subject.as_str(), key.relation.as_str(), key.object.as_str(), revision); + Self::append_event( + &mut inner, + TupleMutation::Remove, + key.subject.as_str(), + key.relation.as_str(), + key.object.as_str(), + revision, + ); Ok(revision) } - fn delete_subject(&self, _partition_id: &PartitionId, subject: &SubjectId) -> AegisResult { - let mut inner = self.inner.lock().map_err(|e| AegisError::Internal(e.to_string()))?; - let subjects: Vec<(String, String, String)> = inner.tuples.keys() + fn delete_subject( + &self, + _partition_id: &PartitionId, + subject: &SubjectId, + ) -> AegisResult { + let mut inner = self + .inner + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; + let subjects: Vec<(String, String, String)> = inner + .tuples + .keys() .filter(|k| k.0 == subject.as_str()) .cloned() .collect(); @@ -144,7 +207,14 @@ impl StorageBackend for InMemoryStorage { for k in subjects { revision = Self::bump_revision(&mut inner); if let Some(tuple) = inner.tuples.remove(&k) { - Self::append_event(&mut inner, TupleMutation::Remove, tuple.subject.as_str(), tuple.relation.as_str(), tuple.object.as_str(), revision); + Self::append_event( + &mut inner, + TupleMutation::Remove, + tuple.subject.as_str(), + tuple.relation.as_str(), + tuple.object.as_str(), + revision, + ); } } if revision == Revision::ZERO { @@ -153,9 +223,18 @@ impl StorageBackend for InMemoryStorage { Ok(revision) } - fn delete_object(&self, _partition_id: &PartitionId, object: &ResourceId) -> AegisResult { - let mut inner = self.inner.lock().map_err(|e| AegisError::Internal(e.to_string()))?; - let objects: Vec<(String, String, String)> = inner.tuples.keys() + fn delete_object( + &self, + _partition_id: &PartitionId, + object: &ResourceId, + ) -> AegisResult { + let mut inner = self + .inner + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; + let objects: Vec<(String, String, String)> = inner + .tuples + .keys() .filter(|k| k.2 == object.as_str()) .cloned() .collect(); @@ -163,7 +242,14 @@ impl StorageBackend for InMemoryStorage { for k in objects { revision = Self::bump_revision(&mut inner); if let Some(tuple) = inner.tuples.remove(&k) { - Self::append_event(&mut inner, TupleMutation::Remove, tuple.subject.as_str(), tuple.relation.as_str(), tuple.object.as_str(), revision); + Self::append_event( + &mut inner, + TupleMutation::Remove, + tuple.subject.as_str(), + tuple.relation.as_str(), + tuple.object.as_str(), + revision, + ); } } if revision == Revision::ZERO { @@ -173,7 +259,10 @@ impl StorageBackend for InMemoryStorage { } fn has_tuple(&self, _partition_id: &PartitionId, key: &TupleKey) -> AegisResult { - let inner = self.inner.lock().map_err(|e| AegisError::Internal(e.to_string()))?; + let inner = self + .inner + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; let k = ( key.subject.as_str().to_string(), key.relation.as_str().to_string(), @@ -182,8 +271,15 @@ impl StorageBackend for InMemoryStorage { Ok(inner.tuples.contains_key(&k)) } - fn read_tuple(&self, _partition_id: &PartitionId, key: &TupleKey) -> AegisResult> { - let inner = self.inner.lock().map_err(|e| AegisError::Internal(e.to_string()))?; + fn read_tuple( + &self, + _partition_id: &PartitionId, + key: &TupleKey, + ) -> AegisResult> { + let inner = self + .inner + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; let k = ( key.subject.as_str().to_string(), key.relation.as_str().to_string(), @@ -199,10 +295,15 @@ impl StorageBackend for InMemoryStorage { relation: Option<&Relation>, _consistency: &crate::types::ConsistencyMode, ) -> AegisResult> { - let inner = self.inner.lock().map_err(|e| AegisError::Internal(e.to_string()))?; - let result: Vec = inner.tuples.values() + let inner = self + .inner + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; + let result: Vec = inner + .tuples + .values() .filter(|t| t.object == *object) - .filter(|t| relation.map_or(true, |r| t.relation == *r)) + .filter(|t| relation.is_none_or(|r| t.relation == *r)) .cloned() .collect(); Ok(result) @@ -215,10 +316,15 @@ impl StorageBackend for InMemoryStorage { relation: Option<&Relation>, _consistency: &crate::types::ConsistencyMode, ) -> AegisResult> { - let inner = self.inner.lock().map_err(|e| AegisError::Internal(e.to_string()))?; - let result: Vec = inner.tuples.values() + let inner = self + .inner + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; + let result: Vec = inner + .tuples + .values() .filter(|t| t.subject == *subject) - .filter(|t| relation.map_or(true, |r| t.relation == *r)) + .filter(|t| relation.is_none_or(|r| t.relation == *r)) .cloned() .collect(); Ok(result) @@ -230,8 +336,13 @@ impl StorageBackend for InMemoryStorage { object: &ResourceId, relation: &Relation, ) -> AegisResult> { - let inner = self.inner.lock().map_err(|e| AegisError::Internal(e.to_string()))?; - let result: Vec = inner.tuples.values() + let inner = self + .inner + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; + let result: Vec = inner + .tuples + .values() .filter(|t| t.object == *object && t.relation == *relation) .cloned() .collect(); @@ -245,19 +356,27 @@ impl StorageBackend for InMemoryStorage { pagination: &PaginationParams, _consistency: &crate::types::ConsistencyMode, ) -> AegisResult { - let inner = self.inner.lock().map_err(|e| AegisError::Internal(e.to_string()))?; - let mut result: Vec = inner.tuples.values() + let inner = self + .inner + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; + let mut result: Vec = inner + .tuples + .values() .filter(|t| { + #[allow(clippy::collapsible_if)] if let Some(ref st) = filter.subject_type { if !t.subject.as_str().starts_with(st.trim_end_matches('#')) { return false; } } + #[allow(clippy::collapsible_if)] if let Some(ref rel) = filter.relation { if t.relation != *rel { return false; } } + #[allow(clippy::collapsible_if)] if let Some(ref ot) = filter.object_type { if !t.object.as_str().starts_with(ot) { return false; @@ -269,7 +388,11 @@ impl StorageBackend for InMemoryStorage { .collect(); let total = result.len(); - let offset = pagination.cursor.as_ref().map(|c| c.offset as usize).unwrap_or(0); + let offset = pagination + .cursor + .as_ref() + .map(|c| c.offset as usize) + .unwrap_or(0); let limit = pagination.limit as usize; let has_more = offset + limit < total; result = result.into_iter().skip(offset).take(limit).collect(); @@ -291,27 +414,45 @@ impl StorageBackend for InMemoryStorage { } fn current_revision(&self, _partition_id: &PartitionId) -> AegisResult { - let inner = self.inner.lock().map_err(|e| AegisError::Internal(e.to_string()))?; + let inner = self + .inner + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; Ok(Revision::new(inner.revision)) } fn read_schema_version(&self) -> AegisResult { - let inner = self.inner.lock().map_err(|e| AegisError::Internal(e.to_string()))?; + let inner = self + .inner + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; Ok(inner.schema_version) } fn write_schema_version(&self, version: u32) -> AegisResult<()> { - let mut inner = self.inner.lock().map_err(|e| AegisError::Internal(e.to_string()))?; + let mut inner = self + .inner + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; inner.schema_version = version; Ok(()) } fn current_token(&self) -> AegisResult { - let inner = self.inner.lock().map_err(|e| AegisError::Internal(e.to_string()))?; - Ok(RevisionToken::new(Revision::new(inner.revision), inner.node_id)) + let inner = self + .inner + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; + Ok(RevisionToken::new( + Revision::new(inner.revision), + inner.node_id, + )) } - fn begin_transaction(&self, _partition_id: &PartitionId) -> AegisResult> { + fn begin_transaction( + &self, + _partition_id: &PartitionId, + ) -> AegisResult> { Ok(Box::new(InMemoryTransaction::new(Arc::clone(&self.inner)))) } @@ -323,11 +464,16 @@ impl StorageBackend for InMemoryStorage { to_revision: Option, _pagination: &PaginationParams, ) -> AegisResult> { - let inner = self.inner.lock().map_err(|e| AegisError::Internal(e.to_string()))?; - let result: Vec = inner.events.iter() - .filter(|e| object.map_or(true, |o| e.object == o.as_str())) - .filter(|e| from_revision.map_or(true, |r| e.revision >= r)) - .filter(|e| to_revision.map_or(true, |r| e.revision <= r)) + let inner = self + .inner + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; + let result: Vec = inner + .events + .iter() + .filter(|e| object.is_none_or(|o| e.object == o.as_str())) + .filter(|e| from_revision.is_none_or(|r| e.revision >= r)) + .filter(|e| to_revision.is_none_or(|r| e.revision <= r)) .cloned() .collect(); Ok(result) @@ -348,8 +494,15 @@ impl StorageBackend for InMemoryStorage { }) } - fn delete_events_before(&self, _partition_id: &PartitionId, cutoff: DateTime) -> AegisResult { - let mut inner = self.inner.lock().map_err(|e| AegisError::Internal(e.to_string()))?; + fn delete_events_before( + &self, + _partition_id: &PartitionId, + cutoff: DateTime, + ) -> AegisResult { + let mut inner = self + .inner + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; let before = inner.events.len(); inner.events.retain(|e| e.timestamp >= cutoff); Ok(before - inner.events.len()) @@ -359,12 +512,23 @@ impl StorageBackend for InMemoryStorage { Ok(0) } - fn delete_soft_deleted_tuples_before(&self, _partition_id: &PartitionId, _cutoff: DateTime) -> AegisResult { + fn delete_soft_deleted_tuples_before( + &self, + _partition_id: &PartitionId, + _cutoff: DateTime, + ) -> AegisResult { Ok(0) } - fn recover_from_events(&self, _partition_id: &PartitionId, to_revision: Option) -> AegisResult { - let mut inner = self.inner.lock().map_err(|e| AegisError::Internal(e.to_string()))?; + fn recover_from_events( + &self, + _partition_id: &PartitionId, + to_revision: Option, + ) -> AegisResult { + let mut inner = self + .inner + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; let events: Vec = inner.events.clone(); inner.tuples.clear(); let mut last_revision = Revision::ZERO; @@ -381,9 +545,9 @@ impl StorageBackend for InMemoryStorage { match event.action { TupleMutation::Add => { let tuple = RelationshipTuple::new( - SubjectId::new(&event.subject).map_err(|e| AegisError::Validation(e))?, - Relation::new(&event.relation).map_err(|e| AegisError::Validation(e))?, - ResourceId::new(&event.object).map_err(|e| AegisError::Validation(e))?, + SubjectId::new(&event.subject).map_err(AegisError::Validation)?, + Relation::new(&event.relation).map_err(AegisError::Validation)?, + ResourceId::new(&event.object).map_err(AegisError::Validation)?, ); inner.tuples.insert(key, tuple); } @@ -403,7 +567,10 @@ impl StorageBackend for InMemoryStorage { events: &[AuditEntry], revision: Revision, ) -> AegisResult<()> { - let mut inner = self.inner.lock().map_err(|e| AegisError::Internal(e.to_string()))?; + let mut inner = self + .inner + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; inner.tuples.clear(); inner.events.clear(); for tuple in tuples { @@ -435,58 +602,97 @@ impl StorageBackend for InMemoryStorage { } fn list_policy_versions(&self) -> AegisResult> { - let inner = self.inner.lock().map_err(|e| AegisError::Internal(e.to_string()))?; + let inner = self + .inner + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; let mut versions: Vec = inner.policy_versions.values().cloned().collect(); versions.sort_by_key(|v| v.version); Ok(versions) } fn save_policy_version(&self, version: &PolicyVersion) -> AegisResult<()> { - let mut inner = self.inner.lock().map_err(|e| AegisError::Internal(e.to_string()))?; - inner.policy_versions.insert(version.version, version.clone()); + let mut inner = self + .inner + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; + inner + .policy_versions + .insert(version.version, version.clone()); Ok(()) } fn load_policy_version(&self, version: u32) -> AegisResult> { - let inner = self.inner.lock().map_err(|e| AegisError::Internal(e.to_string()))?; - Ok(inner.policy_versions.get(&version).map(|v| v.schema.clone())) + let inner = self + .inner + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; + Ok(inner + .policy_versions + .get(&version) + .map(|v| v.schema.clone())) } fn save_policy_draft(&self, draft: &PolicyDraft) -> AegisResult<()> { - let mut inner = self.inner.lock().map_err(|e| AegisError::Internal(e.to_string()))?; - inner.policy_drafts.insert(draft.id.to_string(), draft.clone()); + let mut inner = self + .inner + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; + inner + .policy_drafts + .insert(draft.id.to_string(), draft.clone()); Ok(()) } fn load_policy_draft(&self, id: &str) -> AegisResult> { - let inner = self.inner.lock().map_err(|e| AegisError::Internal(e.to_string()))?; + let inner = self + .inner + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; Ok(inner.policy_drafts.get(id).cloned()) } fn delete_policy_draft(&self, id: &str) -> AegisResult { - let mut inner = self.inner.lock().map_err(|e| AegisError::Internal(e.to_string()))?; + let mut inner = self + .inner + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; Ok(inner.policy_drafts.remove(id).is_some()) } fn save_analysis_schedule(&self, schedule: &AnalysisSchedule) -> AegisResult<()> { - let mut inner = self.inner.lock().map_err(|e| AegisError::Internal(e.to_string()))?; - inner.analysis_schedules.insert(schedule.id.to_string(), schedule.clone()); + let mut inner = self + .inner + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; + inner + .analysis_schedules + .insert(schedule.id.to_string(), schedule.clone()); Ok(()) } fn delete_analysis_schedule(&self, id: &str) -> AegisResult { - let mut inner = self.inner.lock().map_err(|e| AegisError::Internal(e.to_string()))?; + let mut inner = self + .inner + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; Ok(inner.analysis_schedules.remove(id).is_some()) } fn save_analysis_run(&self, run: &AnalysisRun) -> AegisResult<()> { - let mut inner = self.inner.lock().map_err(|e| AegisError::Internal(e.to_string()))?; + let mut inner = self + .inner + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; inner.analysis_runs.push(run.clone()); Ok(()) } fn save_enforcement_event(&self, event: &EnforcementEvent) -> AegisResult<()> { - let mut inner = self.inner.lock().map_err(|e| AegisError::Internal(e.to_string()))?; + let mut inner = self + .inner + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; inner.enforcement_events.push(event.clone()); Ok(()) } @@ -508,7 +714,8 @@ impl InMemoryTransaction { impl StorageTransaction for InMemoryTransaction { fn write(&mut self, _partition_id: &PartitionId, tuple: &RelationshipTuple) -> AegisResult<()> { - self.pending_tuples.push((TupleMutation::Add, tuple.clone())); + self.pending_tuples + .push((TupleMutation::Add, tuple.clone())); Ok(()) } @@ -542,7 +749,10 @@ impl StorageTransaction for InMemoryTransaction { } fn commit(self: Box) -> AegisResult { - let mut inner = self.inner.lock().map_err(|e| AegisError::Internal(e.to_string()))?; + let mut inner = self + .inner + .lock() + .map_err(|e| AegisError::Internal(e.to_string()))?; let mut revision = Revision::ZERO; for (action, tuple) in &self.pending_tuples { let key = ( @@ -667,10 +877,24 @@ mod tests { s.write_tuple(&pid, &t2).unwrap(); s.write_tuple(&pid, &t3).unwrap(); - let by_obj = s.list_by_object(&pid, &ResourceId::new("repo:x").unwrap(), None, &crate::types::ConsistencyMode::MinimizeLatency).unwrap(); + let by_obj = s + .list_by_object( + &pid, + &ResourceId::new("repo:x").unwrap(), + None, + &crate::types::ConsistencyMode::MinimizeLatency, + ) + .unwrap(); assert_eq!(by_obj.len(), 2); - let by_subj = s.list_by_subject(&pid, &SubjectId::new("user:a").unwrap(), None, &crate::types::ConsistencyMode::MinimizeLatency).unwrap(); + let by_subj = s + .list_by_subject( + &pid, + &SubjectId::new("user:a").unwrap(), + None, + &crate::types::ConsistencyMode::MinimizeLatency, + ) + .unwrap(); assert_eq!(by_subj.len(), 2); } @@ -738,7 +962,9 @@ mod tests { }; s.delete_tuple(&pid, &key).unwrap(); - let events = s.query_audit(&pid, None, None, None, &PaginationParams::default()).unwrap(); + let events = s + .query_audit(&pid, None, None, None, &PaginationParams::default()) + .unwrap(); assert_eq!(events.len(), 2); assert_eq!(events[0].action, TupleMutation::Add); diff --git a/crates/aegis-core/src/storage/mod.rs b/crates/aegis-core/src/storage/mod.rs index d34dd6e..099477d 100644 --- a/crates/aegis-core/src/storage/mod.rs +++ b/crates/aegis-core/src/storage/mod.rs @@ -1,25 +1,25 @@ -mod traits; -#[cfg(feature = "sqlite")] -pub mod sqlite; +pub mod async_traits; +#[cfg(target_arch = "wasm32")] +pub mod indexeddb; +pub mod memory; +#[cfg(feature = "mysql")] +pub mod mysql; #[cfg(feature = "postgres")] pub mod postgres; #[cfg(feature = "rocksdb")] pub mod rocksdb; -#[cfg(feature = "mysql")] -pub mod mysql; -pub mod memory; -pub mod async_traits; -#[cfg(target_arch = "wasm32")] -pub mod indexeddb; - #[cfg(feature = "sqlite")] -pub use sqlite::SqliteStorage; +pub mod sqlite; +mod traits; + +pub use memory::InMemoryStorage; +#[cfg(feature = "mysql")] +pub use mysql::MysqlStorage; #[cfg(feature = "postgres")] pub use postgres::PostgresStorage; #[cfg(feature = "rocksdb")] pub use rocksdb::RocksDbStorage; -#[cfg(feature = "mysql")] -pub use mysql::MysqlStorage; -pub use memory::InMemoryStorage; -pub use traits::*; +#[cfg(feature = "sqlite")] +pub use sqlite::SqliteStorage; pub use traits::compute_event_hash; +pub use traits::*; diff --git a/crates/aegis-core/src/storage/mysql.rs b/crates/aegis-core/src/storage/mysql.rs index d94a0d0..4a567a6 100644 --- a/crates/aegis-core/src/storage/mysql.rs +++ b/crates/aegis-core/src/storage/mysql.rs @@ -1,20 +1,21 @@ +use crate::engine::enforcement_history::EnforcementEvent; +use crate::engine::policy_lifecycle::PolicyDraft; +use crate::engine::scheduler::{AnalysisRun, AnalysisSchedule}; use crate::error::{AegisError, AegisResult}; -use crate::util::redact::Redacted; use crate::storage::traits::{ BackendType, IntegrityReport, PolicyVersion, StorageBackend, StorageMeta, StorageTransaction, TupleFilter, }; use crate::types::{ AuditEntry, ConsistencyMode, PaginatedTuples, PaginationCursor, PaginationParams, PartitionId, - Relation, RelationshipTuple, ResourceId, Revision, RevisionToken, SubjectId, TupleKey, TupleMutation, + Relation, RelationshipTuple, ResourceId, Revision, RevisionToken, SubjectId, TupleKey, + TupleMutation, }; +use crate::util::redact::Redacted; use chrono::{DateTime, Utc}; use mysql_async::prelude::Queryable; use std::collections::HashMap; use uuid::Uuid; -use crate::engine::enforcement_history::EnforcementEvent; -use crate::engine::policy_lifecycle::PolicyDraft; -use crate::engine::scheduler::{AnalysisRun, AnalysisSchedule}; /// MySQL configuration. #[derive(Debug, Clone)] @@ -64,14 +65,26 @@ impl MysqlStorage { let scheme = if config.use_tls { "mysqls" } else { "mysql" }; let mut url = format!( "{scheme}://{}:{}@{}:{}/{}", - config.user, config.password.clone().into_inner(), config.host, config.port, config.database + config.user, + config.password.clone().into_inner(), + config.host, + config.port, + config.database ); if config.use_tls { let ssl_ca = config.tls_ca_path.as_ref().map(|p| format!("ssl-ca={}", p)); - let ssl_cert = config.tls_client_cert_path.as_ref().map(|p| format!("ssl-cert={}", p)); - let ssl_key = config.tls_client_key_path.as_ref().map(|p| format!("ssl-key={}", p)); + let ssl_cert = config + .tls_client_cert_path + .as_ref() + .map(|p| format!("ssl-cert={}", p)); + let ssl_key = config + .tls_client_key_path + .as_ref() + .map(|p| format!("ssl-key={}", p)); let params: Vec<&str> = [ssl_ca.as_deref(), ssl_cert.as_deref(), ssl_key.as_deref()] - .into_iter().filter_map(|x| x).collect(); + .into_iter() + .flatten() + .collect(); if !params.is_empty() { url.push('?'); url.push_str(¶ms.join("&")); @@ -210,6 +223,7 @@ impl MysqlStorage { Self::current_revision_async(conn).await } + #[allow(clippy::too_many_arguments)] async fn append_event_async( conn: &mut mysql_async::Conn, revision: Revision, @@ -259,15 +273,15 @@ impl MysqlStorage { created_at: String, metadata_json: Option, ) -> AegisResult { - let subject = SubjectId::new(&subject) - .map_err(|e| AegisError::StorageQuery(e.to_string()))?; - let relation = Relation::new(&relation) - .map_err(|e| AegisError::StorageQuery(e.to_string()))?; - let object = ResourceId::new(&object) - .map_err(|e| AegisError::StorageQuery(e.to_string()))?; + let subject = + SubjectId::new(&subject).map_err(|e| AegisError::StorageQuery(e.to_string()))?; + let relation = + Relation::new(&relation).map_err(|e| AegisError::StorageQuery(e.to_string()))?; + let object = + ResourceId::new(&object).map_err(|e| AegisError::StorageQuery(e.to_string()))?; let created_at: DateTime = created_at.parse().unwrap_or_else(|_| Utc::now()); - let metadata = metadata_json - .and_then(|m| serde_json::from_str::>(&m).ok()); + let metadata = + metadata_json.and_then(|m| serde_json::from_str::>(&m).ok()); Ok(RelationshipTuple { subject, relation, @@ -306,7 +320,11 @@ impl StorageBackend for MysqlStorage { BackendType::Mysql } - fn write_tuple(&self, partition_id: &PartitionId, tuple: &RelationshipTuple) -> AegisResult { + fn write_tuple( + &self, + partition_id: &PartitionId, + tuple: &RelationshipTuple, + ) -> AegisResult { self.runtime.block_on(async { let mut conn = self.get_conn().await?; let revision = Self::bump_revision_async(&mut conn).await?; @@ -345,7 +363,11 @@ impl StorageBackend for MysqlStorage { }) } - fn write_tuples_batch(&self, partition_id: &PartitionId, tuples: &[RelationshipTuple]) -> AegisResult { + fn write_tuples_batch( + &self, + partition_id: &PartitionId, + tuples: &[RelationshipTuple], + ) -> AegisResult { if tuples.is_empty() { return self.current_revision(partition_id); } @@ -426,7 +448,11 @@ impl StorageBackend for MysqlStorage { }) } - fn delete_subject(&self, partition_id: &PartitionId, subject: &SubjectId) -> AegisResult { + fn delete_subject( + &self, + partition_id: &PartitionId, + subject: &SubjectId, + ) -> AegisResult { let subj = subject.as_str().to_string(); self.runtime.block_on(async { let mut conn = self.get_conn().await?; @@ -457,8 +483,14 @@ impl StorageBackend for MysqlStorage { let identity = self.actor_identity.lock().unwrap().clone(); for (relation, object) in &rows { Self::append_event_async( - &mut conn, revision, "remove", subject.as_str(), - relation, object, None, identity.as_deref(), + &mut conn, + revision, + "remove", + subject.as_str(), + relation, + object, + None, + identity.as_deref(), ) .await?; } @@ -467,7 +499,11 @@ impl StorageBackend for MysqlStorage { }) } - fn delete_object(&self, partition_id: &PartitionId, object: &ResourceId) -> AegisResult { + fn delete_object( + &self, + partition_id: &PartitionId, + object: &ResourceId, + ) -> AegisResult { let obj = object.as_str().to_string(); self.runtime.block_on(async { let mut conn = self.get_conn().await?; @@ -498,8 +534,14 @@ impl StorageBackend for MysqlStorage { let identity = self.actor_identity.lock().unwrap().clone(); for (subject, relation) in &rows { Self::append_event_async( - &mut conn, revision, "remove", subject, - relation, object.as_str(), None, identity.as_deref(), + &mut conn, + revision, + "remove", + subject, + relation, + object.as_str(), + None, + identity.as_deref(), ) .await?; } @@ -523,7 +565,11 @@ impl StorageBackend for MysqlStorage { }) } - fn read_tuple(&self, partition_id: &PartitionId, key: &TupleKey) -> AegisResult> { + fn read_tuple( + &self, + partition_id: &PartitionId, + key: &TupleKey, + ) -> AegisResult> { self.runtime.block_on(async { let mut conn = self.get_conn().await?; let rows: Vec<(String, String, String, String, Option)> = conn @@ -545,14 +591,20 @@ impl StorageBackend for MysqlStorage { } fn list_by_object( - &self, partition_id: &PartitionId, object: &ResourceId, relation: Option<&Relation>, consistency: &ConsistencyMode, + &self, + partition_id: &PartitionId, + object: &ResourceId, + relation: Option<&Relation>, + consistency: &ConsistencyMode, ) -> AegisResult> { let obj = object.as_str().to_string(); let rel = relation.map(|r| r.as_str().to_string()); let rev_filter = match consistency { ConsistencyMode::AtRevision(rev) => { let r = rev.as_u64() as i64; - format!("`revision_added` <= {r} AND (`revision_removed` IS NULL OR `revision_removed` > {r})") + format!( + "`revision_added` <= {r} AND (`revision_removed` IS NULL OR `revision_removed` > {r})" + ) } _ => "`revision_removed` IS NULL".to_string(), }; @@ -562,7 +614,7 @@ impl StorageBackend for MysqlStorage { let rows: Vec<(String, String, String, String, Option)> = if let Some(ref r) = rel { conn.exec( - &format!( + format!( "SELECT `subject`, `relation`, `object`, `created_at`, `metadata` FROM _aegis_tuples WHERE `object` = ? AND `relation` = ? AND `partition_id` = ? AND {rev_filter}" ), @@ -571,7 +623,7 @@ impl StorageBackend for MysqlStorage { .await } else { conn.exec( - &format!( + format!( "SELECT `subject`, `relation`, `object`, `created_at`, `metadata` FROM _aegis_tuples WHERE `object` = ? AND `partition_id` = ? AND {rev_filter}" ), @@ -590,14 +642,20 @@ impl StorageBackend for MysqlStorage { } fn list_by_subject( - &self, partition_id: &PartitionId, subject: &SubjectId, relation: Option<&Relation>, consistency: &ConsistencyMode, + &self, + partition_id: &PartitionId, + subject: &SubjectId, + relation: Option<&Relation>, + consistency: &ConsistencyMode, ) -> AegisResult> { let subj = subject.as_str().to_string(); let rel = relation.map(|r| r.as_str().to_string()); let rev_filter = match consistency { ConsistencyMode::AtRevision(rev) => { let r = rev.as_u64() as i64; - format!("`revision_added` <= {r} AND (`revision_removed` IS NULL OR `revision_removed` > {r})") + format!( + "`revision_added` <= {r} AND (`revision_removed` IS NULL OR `revision_removed` > {r})" + ) } _ => "`revision_removed` IS NULL".to_string(), }; @@ -607,7 +665,7 @@ impl StorageBackend for MysqlStorage { let rows: Vec<(String, String, String, String, Option)> = if let Some(ref r) = rel { conn.exec( - &format!( + format!( "SELECT `subject`, `relation`, `object`, `created_at`, `metadata` FROM _aegis_tuples WHERE `subject` = ? AND `relation` = ? AND `partition_id` = ? AND {rev_filter}" ), @@ -616,7 +674,7 @@ impl StorageBackend for MysqlStorage { .await } else { conn.exec( - &format!( + format!( "SELECT `subject`, `relation`, `object`, `created_at`, `metadata` FROM _aegis_tuples WHERE `subject` = ? AND `partition_id` = ? AND {rev_filter}" ), @@ -635,7 +693,10 @@ impl StorageBackend for MysqlStorage { } fn list_by_relation( - &self, partition_id: &PartitionId, object: &ResourceId, relation: &Relation, + &self, + partition_id: &PartitionId, + object: &ResourceId, + relation: &Relation, ) -> AegisResult> { let obj = object.as_str().to_string(); let rel = relation.as_str().to_string(); @@ -660,7 +721,11 @@ impl StorageBackend for MysqlStorage { } fn query_tuples( - &self, partition_id: &PartitionId, filter: &TupleFilter, pagination: &PaginationParams, consistency: &ConsistencyMode, + &self, + partition_id: &PartitionId, + filter: &TupleFilter, + pagination: &PaginationParams, + consistency: &ConsistencyMode, ) -> AegisResult { let subj_type = filter.subject_type.clone(); let rel = filter.relation.as_ref().map(|r| r.as_str().to_string()); @@ -669,7 +734,9 @@ impl StorageBackend for MysqlStorage { let rev_filter = match consistency { ConsistencyMode::AtRevision(rev) => { let r = rev.as_u64() as i64; - format!("`revision_added` <= {r} AND (`revision_removed` IS NULL OR `revision_removed` > {r})") + format!( + "`revision_added` <= {r} AND (`revision_removed` IS NULL OR `revision_removed` > {r})" + ) } _ => "`revision_removed` IS NULL".to_string(), }; @@ -678,7 +745,7 @@ impl StorageBackend for MysqlStorage { let mut conn = self.get_conn().await?; let revision = Self::current_revision_async(&mut conn).await?; - let mut conditions = vec![format!("`partition_id` = ?1"), rev_filter]; + let mut conditions = vec!["`partition_id` = ?1".to_string(), rev_filter]; let mut values: Vec = vec![partition_id.as_str().into()]; if let Some(st) = subj_type { @@ -722,7 +789,13 @@ impl StorageBackend for MysqlStorage { let mut tuples = Vec::with_capacity(rows.len()); for (subject_str, relation_str, object_str, created_at, metadata_json) in rows { - tuples.push(Self::row_to_tuple(subject_str, relation_str, object_str, created_at, metadata_json)?); + tuples.push(Self::row_to_tuple( + subject_str, + relation_str, + object_str, + created_at, + metadata_json, + )?); } let next_cursor = if tuples.len() as u64 == limit { @@ -734,7 +807,11 @@ impl StorageBackend for MysqlStorage { None }; - Ok(PaginatedTuples { tuples, next_cursor, revision }) + Ok(PaginatedTuples { + tuples, + next_cursor, + revision, + }) }) } @@ -751,7 +828,10 @@ impl StorageBackend for MysqlStorage { Ok(RevisionToken::new(revision, self.node_id)) } - fn begin_transaction(&self, partition_id: &PartitionId) -> AegisResult> { + fn begin_transaction( + &self, + partition_id: &PartitionId, + ) -> AegisResult> { let _ = partition_id; let node_id = self.node_id; let handle = self.runtime.handle().clone(); @@ -761,13 +841,20 @@ impl StorageBackend for MysqlStorage { conn.exec_drop("BEGIN", ()) .await .map_err(|e| AegisError::StorageQuery(e.to_string()))?; - Ok(Box::new(MysqlTransaction::new(conn, handle, node_id, identity)) as Box) + Ok( + Box::new(MysqlTransaction::new(conn, handle, node_id, identity)) + as Box, + ) }) } fn query_audit( - &self, partition_id: &PartitionId, object: Option<&ResourceId>, from_revision: Option, - to_revision: Option, pagination: &PaginationParams, + &self, + partition_id: &PartitionId, + object: Option<&ResourceId>, + from_revision: Option, + to_revision: Option, + pagination: &PaginationParams, ) -> AegisResult> { let from = from_revision.map(|r| r.as_u64() as i64); let to = to_revision.map(|r| r.as_u64() as i64); @@ -809,6 +896,7 @@ impl StorageBackend for MysqlStorage { )); let params = mysql_async::Params::Positional(values); + #[allow(clippy::type_complexity)] let rows: Vec<(i64, String, String, String, String, String, Option, Option)> = conn .exec(&sql, params) .await @@ -873,7 +961,10 @@ impl StorageBackend for MysqlStorage { fn integrity_check(&self) -> AegisResult { self.runtime.block_on(async { let mut conn = self.get_conn().await?; - match conn.exec_drop("SELECT 1 FROM _aegis_meta LIMIT 1", ()).await { + match conn + .exec_drop("SELECT 1 FROM _aegis_meta LIMIT 1", ()) + .await + { Ok(_) => Ok(IntegrityReport { passed: true, details: vec!["ok".to_string()], @@ -894,7 +985,11 @@ impl StorageBackend for MysqlStorage { }) } - fn delete_events_before(&self, partition_id: &PartitionId, cutoff: DateTime) -> AegisResult { + fn delete_events_before( + &self, + partition_id: &PartitionId, + cutoff: DateTime, + ) -> AegisResult { self.runtime.block_on(async { let mut conn = self.get_conn().await?; let cutoff_str = cutoff.to_rfc3339(); @@ -909,7 +1004,11 @@ impl StorageBackend for MysqlStorage { }) } - fn delete_soft_deleted_tuples_before(&self, partition_id: &PartitionId, cutoff: DateTime) -> AegisResult { + fn delete_soft_deleted_tuples_before( + &self, + partition_id: &PartitionId, + cutoff: DateTime, + ) -> AegisResult { self.runtime.block_on(async { let mut conn = self.get_conn().await?; let cutoff_str = cutoff.to_rfc3339(); @@ -930,7 +1029,11 @@ impl StorageBackend for MysqlStorage { }) } - fn recover_from_events(&self, partition_id: &PartitionId, to_revision: Option) -> AegisResult { + fn recover_from_events( + &self, + partition_id: &PartitionId, + to_revision: Option, + ) -> AegisResult { self.runtime.block_on(async { let mut conn = self.get_conn().await?; @@ -953,6 +1056,7 @@ impl StorageBackend for MysqlStorage { for (rev, action, subject, relation, object, metadata) in &rows { let revision = Revision::new(*rev as u64); + #[allow(clippy::collapsible_if)] if let Some(target) = to_revision { if revision > target { continue; @@ -1060,6 +1164,7 @@ impl StorageBackend for MysqlStorage { let _ = partition_id; self.runtime.block_on(async { let mut conn = self.get_conn().await?; + #[allow(clippy::type_complexity)] let rows: Vec<(i64, i64, String, String, String, String, Option, String, Option, String, String)> = conn .exec( "SELECT `event_id`, `revision`, `action`, `subject`, `relation`, `object`, `metadata`, `timestamp`, `identity`, `previous_hash`, `event_hash` @@ -1373,7 +1478,10 @@ impl MysqlTransaction { .ok_or_else(|| AegisError::Internal("transaction already consumed".into())) } - fn block_on(&self, fut: impl std::future::Future>) -> AegisResult { + fn block_on( + &self, + fut: impl std::future::Future>, + ) -> AegisResult { self.runtime.block_on(fut) } @@ -1395,6 +1503,7 @@ impl MysqlTransaction { Ok(Revision::new(rev as u64)) } + #[allow(clippy::too_many_arguments)] async fn append_event_async( conn: &mut mysql_async::Conn, revision: Revision, @@ -1491,9 +1600,14 @@ impl MysqlTransaction { .map_err(|e| AegisError::StorageQuery(e.to_string()))?; Self::append_event_async( - conn, revision, "add", tuple.subject.as_str(), - tuple.relation.as_str(), tuple.object.as_str(), - metadata_json.as_deref(), identity, + conn, + revision, + "add", + tuple.subject.as_str(), + tuple.relation.as_str(), + tuple.object.as_str(), + metadata_json.as_deref(), + identity, ) .await?; @@ -1517,8 +1631,14 @@ impl MysqlTransaction { .map_err(|e| AegisError::StorageQuery(e.to_string()))?; Self::append_event_async( - conn, revision, "remove", key.subject.as_str(), - key.relation.as_str(), key.object.as_str(), None, identity, + conn, + revision, + "remove", + key.subject.as_str(), + key.relation.as_str(), + key.object.as_str(), + None, + identity, ) .await?; @@ -1565,7 +1685,7 @@ impl StorageTransaction for MysqlTransaction { let mutex = self.conn_ref()?; self.block_on(async { let mut conn = mutex.lock().await; - conn.exec_drop(&format!("SAVEPOINT \"{}\"", name_owned), ()) + conn.exec_drop(format!("SAVEPOINT \"{}\"", name_owned), ()) .await .map_err(|e| AegisError::StorageQuery(e.to_string()))?; Ok(()) @@ -1578,7 +1698,7 @@ impl StorageTransaction for MysqlTransaction { let mutex = self.conn_ref()?; self.block_on(async { let mut conn = mutex.lock().await; - conn.exec_drop(&format!("ROLLBACK TO SAVEPOINT \"{}\"", name_owned), ()) + conn.exec_drop(format!("ROLLBACK TO SAVEPOINT \"{}\"", name_owned), ()) .await .map_err(|e| AegisError::StorageQuery(e.to_string()))?; Ok(()) @@ -1591,7 +1711,7 @@ impl StorageTransaction for MysqlTransaction { let mutex = self.conn_ref()?; self.block_on(async { let mut conn = mutex.lock().await; - conn.exec_drop(&format!("RELEASE SAVEPOINT \"{}\"", name_owned), ()) + conn.exec_drop(format!("RELEASE SAVEPOINT \"{}\"", name_owned), ()) .await .map_err(|e| AegisError::StorageQuery(e.to_string()))?; Ok(()) @@ -1600,7 +1720,8 @@ impl StorageTransaction for MysqlTransaction { fn commit(self: Box) -> AegisResult { let s = *self; - let mutex = s.conn + let mutex = s + .conn .ok_or_else(|| AegisError::Internal("transaction already consumed".into()))?; let handle = s.runtime; handle.block_on(async { @@ -1622,7 +1743,8 @@ impl StorageTransaction for MysqlTransaction { fn rollback(self: Box) -> AegisResult<()> { let s = *self; - let mutex = s.conn + let mutex = s + .conn .ok_or_else(|| AegisError::Internal("transaction already consumed".into()))?; let handle = s.runtime; handle.block_on(async { diff --git a/crates/aegis-core/src/storage/postgres.rs b/crates/aegis-core/src/storage/postgres.rs index c76f3f8..276a260 100644 --- a/crates/aegis-core/src/storage/postgres.rs +++ b/crates/aegis-core/src/storage/postgres.rs @@ -1,11 +1,11 @@ +use crate::engine::enforcement_history::EnforcementEvent; +use crate::engine::policy_lifecycle::PolicyDraft; +use crate::engine::scheduler::{AnalysisRun, AnalysisSchedule}; use crate::error::{AegisError, AegisResult}; use crate::storage::traits::{ BackendType, IntegrityReport, PolicyVersion, StorageBackend, StorageMeta, StorageTransaction, TupleFilter, }; -use crate::engine::enforcement_history::EnforcementEvent; -use crate::engine::policy_lifecycle::PolicyDraft; -use crate::engine::scheduler::{AnalysisRun, AnalysisSchedule}; use crate::types::{ AuditEntry, ConsistencyMode, PaginatedTuples, PaginationCursor, PaginationParams, PartitionId, Relation, RelationshipTuple, ResourceId, Revision, RevisionToken, SubjectId, TupleKey, @@ -157,7 +157,10 @@ impl PostgresStorage { Ok(()) } - async fn current_revision_async(client: &tokio_postgres::Client, partition_id: &PartitionId) -> AegisResult { + async fn current_revision_async( + client: &tokio_postgres::Client, + partition_id: &PartitionId, + ) -> AegisResult { let key = format!("revision:{}", partition_id.as_str()); let row = client .query_one( @@ -170,7 +173,10 @@ impl PostgresStorage { Ok(Revision::new(rev as u64)) } - async fn bump_revision_async(client: &tokio_postgres::Client, partition_id: &PartitionId) -> AegisResult { + async fn bump_revision_async( + client: &tokio_postgres::Client, + partition_id: &PartitionId, + ) -> AegisResult { let key = format!("revision:{}", partition_id.as_str()); client .execute( @@ -190,6 +196,7 @@ impl PostgresStorage { Ok(Revision::new(rev as u64)) } + #[allow(clippy::too_many_arguments)] async fn append_event_async( client: &tokio_postgres::Client, partition_id: &PartitionId, @@ -237,10 +244,16 @@ impl PostgresStorage { /// Add ALTER TABLE for existing databases that lack the hash columns. async fn add_hash_columns_async(client: &tokio_postgres::Client) -> AegisResult<()> { let _ = client - .execute("ALTER TABLE _aegis_events ADD COLUMN previous_hash TEXT NOT NULL DEFAULT ''", &[]) + .execute( + "ALTER TABLE _aegis_events ADD COLUMN previous_hash TEXT NOT NULL DEFAULT ''", + &[], + ) .await; let _ = client - .execute("ALTER TABLE _aegis_events ADD COLUMN event_hash TEXT NOT NULL DEFAULT ''", &[]) + .execute( + "ALTER TABLE _aegis_events ADD COLUMN event_hash TEXT NOT NULL DEFAULT ''", + &[], + ) .await; Ok(()) } @@ -273,14 +286,18 @@ impl StorageBackend for PostgresStorage { prev } - fn write_tuple(&self, partition_id: &PartitionId, tuple: &RelationshipTuple) -> AegisResult { + fn write_tuple( + &self, + partition_id: &PartitionId, + tuple: &RelationshipTuple, + ) -> AegisResult { self.runtime.block_on(async { let client = self.get_client().await?; let revision = Self::bump_revision_async(&client, partition_id).await?; let meta_val = tuple .metadata .as_ref() - .map(|m| serde_json::to_value(m)) + .map(serde_json::to_value) .transpose() .map_err(|e| AegisError::MetadataValidation(e.to_string()))?; @@ -313,7 +330,11 @@ impl StorageBackend for PostgresStorage { }) } - fn write_tuples_batch(&self, partition_id: &PartitionId, tuples: &[RelationshipTuple]) -> AegisResult { + fn write_tuples_batch( + &self, + partition_id: &PartitionId, + tuples: &[RelationshipTuple], + ) -> AegisResult { if tuples.is_empty() { return self.current_revision(partition_id); } @@ -325,9 +346,9 @@ impl StorageBackend for PostgresStorage { let meta_val = tuple .metadata .as_ref() - .map(|m| serde_json::to_value(m)) - .transpose() - .map_err(|e| AegisError::MetadataValidation(e.to_string()))?; + .map(serde_json::to_value) + .transpose() + .map_err(|e| AegisError::MetadataValidation(e.to_string()))?; client .execute( @@ -396,7 +417,11 @@ impl StorageBackend for PostgresStorage { }) } - fn delete_subject(&self, partition_id: &PartitionId, subject: &SubjectId) -> AegisResult { + fn delete_subject( + &self, + partition_id: &PartitionId, + subject: &SubjectId, + ) -> AegisResult { let subj = subject.as_str().to_string(); self.runtime.block_on(async { let client = self.get_client().await?; @@ -430,7 +455,15 @@ impl StorageBackend for PostgresStorage { let identity = self.actor_identity.lock().unwrap().clone(); for (relation, object) in &tuples { Self::append_event_async( - &client, partition_id, revision, "remove", subject.as_str(), relation, object, None, identity.as_deref(), + &client, + partition_id, + revision, + "remove", + subject.as_str(), + relation, + object, + None, + identity.as_deref(), ) .await?; } @@ -439,7 +472,11 @@ impl StorageBackend for PostgresStorage { }) } - fn delete_object(&self, partition_id: &PartitionId, object: &ResourceId) -> AegisResult { + fn delete_object( + &self, + partition_id: &PartitionId, + object: &ResourceId, + ) -> AegisResult { let obj = object.as_str().to_string(); self.runtime.block_on(async { let client = self.get_client().await?; @@ -473,7 +510,15 @@ impl StorageBackend for PostgresStorage { let identity = self.actor_identity.lock().unwrap().clone(); for (subject, relation) in &tuples { Self::append_event_async( - &client, partition_id, revision, "remove", subject, relation, object.as_str(), None, identity.as_deref(), + &client, + partition_id, + revision, + "remove", + subject, + relation, + object.as_str(), + None, + identity.as_deref(), ) .await?; } @@ -498,7 +543,11 @@ impl StorageBackend for PostgresStorage { }) } - fn read_tuple(&self, partition_id: &PartitionId, key: &TupleKey) -> AegisResult> { + fn read_tuple( + &self, + partition_id: &PartitionId, + key: &TupleKey, + ) -> AegisResult> { self.runtime.block_on(async { let client = self.get_client().await?; let rows = client @@ -542,7 +591,11 @@ impl StorageBackend for PostgresStorage { } fn list_by_object( - &self, partition_id: &PartitionId, object: &ResourceId, relation: Option<&Relation>, consistency: &ConsistencyMode, + &self, + partition_id: &PartitionId, + object: &ResourceId, + relation: Option<&Relation>, + consistency: &ConsistencyMode, ) -> AegisResult> { let obj = object.as_str().to_string(); let rel = relation.map(|r| r.as_str().to_string()); @@ -550,7 +603,9 @@ impl StorageBackend for PostgresStorage { let rev_filter = match consistency { ConsistencyMode::AtRevision(rev) => { let r = rev.as_u64() as i64; - format!("revision_added <= {r} AND (revision_removed IS NULL OR revision_removed > {r})") + format!( + "revision_added <= {r} AND (revision_removed IS NULL OR revision_removed > {r})" + ) } _ => "revision_removed IS NULL".to_string(), }; @@ -640,7 +695,11 @@ impl StorageBackend for PostgresStorage { } fn list_by_subject( - &self, partition_id: &PartitionId, subject: &SubjectId, relation: Option<&Relation>, consistency: &ConsistencyMode, + &self, + partition_id: &PartitionId, + subject: &SubjectId, + relation: Option<&Relation>, + consistency: &ConsistencyMode, ) -> AegisResult> { let subj = subject.as_str().to_string(); let rel = relation.map(|r| r.as_str().to_string()); @@ -648,7 +707,9 @@ impl StorageBackend for PostgresStorage { let rev_filter = match consistency { ConsistencyMode::AtRevision(rev) => { let r = rev.as_u64() as i64; - format!("revision_added <= {r} AND (revision_removed IS NULL OR revision_removed > {r})") + format!( + "revision_added <= {r} AND (revision_removed IS NULL OR revision_removed > {r})" + ) } _ => "revision_removed IS NULL".to_string(), }; @@ -738,7 +799,10 @@ impl StorageBackend for PostgresStorage { } fn list_by_relation( - &self, partition_id: &PartitionId, object: &ResourceId, relation: &Relation, + &self, + partition_id: &PartitionId, + object: &ResourceId, + relation: &Relation, ) -> AegisResult> { let obj = object.as_str().to_string(); let rel = relation.as_str().to_string(); @@ -782,7 +846,11 @@ impl StorageBackend for PostgresStorage { } fn query_tuples( - &self, partition_id: &PartitionId, filter: &TupleFilter, pagination: &PaginationParams, consistency: &ConsistencyMode, + &self, + partition_id: &PartitionId, + filter: &TupleFilter, + pagination: &PaginationParams, + consistency: &ConsistencyMode, ) -> AegisResult { let subj_type = filter.subject_type.clone(); let rel = filter.relation.as_ref().map(|r| r.as_str().to_string()); @@ -791,7 +859,9 @@ impl StorageBackend for PostgresStorage { let rev_filter = match consistency { ConsistencyMode::AtRevision(rev) => { let r = rev.as_u64() as i64; - format!("revision_added <= {r} AND (revision_removed IS NULL OR revision_removed > {r})") + format!( + "revision_added <= {r} AND (revision_removed IS NULL OR revision_removed > {r})" + ) } _ => "revision_removed IS NULL".to_string(), }; @@ -858,7 +928,9 @@ impl StorageBackend for PostgresStorage { .query(&sql, ¶m_refs) .await .map_err(|e| AegisError::StorageQuery(e.to_string()))?; - tx.commit().await.map_err(|e| AegisError::StorageQuery(e.to_string()))?; + tx.commit() + .await + .map_err(|e| AegisError::StorageQuery(e.to_string()))?; result } else { client @@ -874,7 +946,8 @@ impl StorageBackend for PostgresStorage { let object_str: String = row.get("object"); let created: DateTime = row.get("created_at"); let meta_val: Option = row.get("metadata"); - let metadata = meta_val.and_then(|v| serde_json::from_value::>(v).ok()); + let metadata = meta_val + .and_then(|v| serde_json::from_value::>(v).ok()); let subject = SubjectId::new(&subject_str) .map_err(|e| AegisError::StorageQuery(e.to_string()))?; @@ -903,7 +976,11 @@ impl StorageBackend for PostgresStorage { None }; - Ok(PaginatedTuples { tuples, next_cursor, revision }) + Ok(PaginatedTuples { + tuples, + next_cursor, + revision, + }) }) } @@ -919,7 +996,10 @@ impl StorageBackend for PostgresStorage { Ok(RevisionToken::new(revision, self.node_id)) } - fn begin_transaction(&self, _partition_id: &PartitionId) -> AegisResult> { + fn begin_transaction( + &self, + _partition_id: &PartitionId, + ) -> AegisResult> { let node_id = self.node_id; let handle = self.runtime.handle().clone(); let identity = self.actor_identity.lock().unwrap().clone(); @@ -929,13 +1009,20 @@ impl StorageBackend for PostgresStorage { .execute("BEGIN", &[]) .await .map_err(|e| AegisError::StorageQuery(e.to_string()))?; - Ok(Box::new(PostgresTransaction::new(client, handle, node_id, identity)) as Box) + Ok( + Box::new(PostgresTransaction::new(client, handle, node_id, identity)) + as Box, + ) }) } fn query_audit( - &self, partition_id: &PartitionId, object: Option<&ResourceId>, from_revision: Option, - to_revision: Option, pagination: &PaginationParams, + &self, + partition_id: &PartitionId, + object: Option<&ResourceId>, + from_revision: Option, + to_revision: Option, + pagination: &PaginationParams, ) -> AegisResult> { let from = from_revision.map(|r| r.as_u64() as i64); let to = to_revision.map(|r| r.as_u64() as i64); @@ -1001,8 +1088,8 @@ impl StorageBackend for PostgresStorage { let obj: String = row.get("object"); let ts: DateTime = row.get("timestamp"); let meta_val: Option = row.get("metadata"); - let metadata = - meta_val.and_then(|v| serde_json::from_value::>(v).ok()); + let metadata = meta_val + .and_then(|v| serde_json::from_value::>(v).ok()); let identity: Option = row.get("identity"); let action = if action_str == "add" { TupleMutation::Add @@ -1030,7 +1117,10 @@ impl StorageBackend for PostgresStorage { self.runtime.block_on(async { let client = self.get_client().await?; match client - .query_opt("SELECT version FROM _aegis_schema ORDER BY version DESC LIMIT 1", &[]) + .query_opt( + "SELECT version FROM _aegis_schema ORDER BY version DESC LIMIT 1", + &[], + ) .await { Ok(Some(row)) => { @@ -1038,7 +1128,9 @@ impl StorageBackend for PostgresStorage { Ok(v as u32) } Ok(None) => Ok(0), - Err(ref e) if e.code() == Some(&tokio_postgres::error::SqlState::UNDEFINED_TABLE) => { + Err(ref e) + if e.code() == Some(&tokio_postgres::error::SqlState::UNDEFINED_TABLE) => + { Ok(0) } Err(e) => Err(AegisError::StorageQuery(e.to_string())), @@ -1063,7 +1155,10 @@ impl StorageBackend for PostgresStorage { fn integrity_check(&self) -> AegisResult { self.runtime.block_on(async { let client = self.get_client().await?; - match client.query_one("SELECT 1 FROM _aegis_meta LIMIT 1", &[]).await { + match client + .query_one("SELECT 1 FROM _aegis_meta LIMIT 1", &[]) + .await + { Ok(_) => Ok(IntegrityReport { passed: true, details: vec!["ok".to_string()], @@ -1084,18 +1179,29 @@ impl StorageBackend for PostgresStorage { }) } - fn delete_events_before(&self, partition_id: &PartitionId, cutoff: DateTime) -> AegisResult { + fn delete_events_before( + &self, + partition_id: &PartitionId, + cutoff: DateTime, + ) -> AegisResult { self.runtime.block_on(async { let client = self.get_client().await?; let rows = client - .execute("DELETE FROM _aegis_events WHERE partition_id = $1 AND timestamp < $2", &[&partition_id.as_str(), &cutoff]) + .execute( + "DELETE FROM _aegis_events WHERE partition_id = $1 AND timestamp < $2", + &[&partition_id.as_str(), &cutoff], + ) .await .map_err(|e| AegisError::StorageQuery(e.to_string()))?; Ok(rows as usize) }) } - fn delete_soft_deleted_tuples_before(&self, partition_id: &PartitionId, cutoff: DateTime) -> AegisResult { + fn delete_soft_deleted_tuples_before( + &self, + partition_id: &PartitionId, + cutoff: DateTime, + ) -> AegisResult { self.runtime.block_on(async { let client = self.get_client().await?; let rows = client @@ -1115,7 +1221,11 @@ impl StorageBackend for PostgresStorage { }) } - fn recover_from_events(&self, partition_id: &PartitionId, to_revision: Option) -> AegisResult { + fn recover_from_events( + &self, + partition_id: &PartitionId, + to_revision: Option, + ) -> AegisResult { self.runtime.block_on(async { let client = self.get_client().await?; @@ -1149,6 +1259,7 @@ impl StorageBackend for PostgresStorage { let meta_val: Option = row.get(5); let revision = Revision::new(rev as u64); + #[allow(clippy::collapsible_if)] if let Some(target) = to_revision { if revision > target { continue; @@ -1342,7 +1453,7 @@ impl StorageBackend for PostgresStorage { for tuple in tuples { let meta_val = tuple.metadata .as_ref() - .map(|m| serde_json::to_value(m)) + .map(serde_json::to_value) .transpose() .map_err(|e| AegisError::MetadataValidation(e.to_string()))?; let revision_added: i64 = revision.as_u64() as i64; @@ -1363,7 +1474,7 @@ impl StorageBackend for PostgresStorage { }; let meta_val = event.metadata .as_ref() - .map(|m| serde_json::to_value(m)) + .map(serde_json::to_value) .transpose() .map_err(|e| AegisError::MetadataValidation(e.to_string()))?; client @@ -1458,7 +1569,13 @@ impl StorageBackend for PostgresStorage { "INSERT INTO _aegis_policy_drafts (id, status, created_at, updated_at, data) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (id) DO UPDATE SET status=$2, updated_at=$4, data=$5", - &[&draft.id.to_string(), &draft.status.to_string(), &Utc::now(), &Utc::now(), &data], + &[ + &draft.id.to_string(), + &draft.status.to_string(), + &Utc::now(), + &Utc::now(), + &data, + ], ) .await .map_err(|e| AegisError::StorageQuery(e.to_string()))?; @@ -1492,7 +1609,10 @@ impl StorageBackend for PostgresStorage { self.runtime.block_on(async { let client = self.get_client().await?; let row = client - .query_opt("DELETE FROM _aegis_policy_drafts WHERE id = $1 RETURNING id", &[&id]) + .query_opt( + "DELETE FROM _aegis_policy_drafts WHERE id = $1 RETURNING id", + &[&id], + ) .await .map_err(|e| AegisError::StorageQuery(e.to_string()))?; Ok(row.is_some()) @@ -1520,7 +1640,10 @@ impl StorageBackend for PostgresStorage { self.runtime.block_on(async { let client = self.get_client().await?; let rows = client - .execute("DELETE FROM _aegis_analysis_schedules WHERE id = $1", &[&id]) + .execute( + "DELETE FROM _aegis_analysis_schedules WHERE id = $1", + &[&id], + ) .await .map_err(|e| AegisError::StorageQuery(e.to_string()))?; Ok(rows > 0) @@ -1593,11 +1716,17 @@ impl PostgresTransaction { .ok_or_else(|| AegisError::Internal("transaction already consumed".into())) } - fn block_on(&self, fut: impl std::future::Future>) -> AegisResult { + fn block_on( + &self, + fut: impl std::future::Future>, + ) -> AegisResult { self.runtime.block_on(fut) } - async fn bump_revision_async(client: &tokio_postgres::Client, partition_id: &PartitionId) -> AegisResult { + async fn bump_revision_async( + client: &tokio_postgres::Client, + partition_id: &PartitionId, + ) -> AegisResult { let key = format!("revision:{}", partition_id.as_str()); client .execute( @@ -1617,6 +1746,7 @@ impl PostgresTransaction { Ok(Revision::new(rev as u64)) } + #[allow(clippy::too_many_arguments)] async fn append_event_async( client: &tokio_postgres::Client, partition_id: &PartitionId, @@ -1704,7 +1834,7 @@ impl StorageTransaction for PostgresTransaction { let meta_val = tuple_clone .metadata .as_ref() - .map(|m| serde_json::to_value(m)) + .map(serde_json::to_value) .transpose() .map_err(|e| AegisError::MetadataValidation(e.to_string()))?; @@ -1768,8 +1898,7 @@ impl StorageTransaction for PostgresTransaction { let name_owned = name.to_string(); self.block_on(async { let conn = self.conn.as_ref().unwrap(); - conn - .execute(&format!("SAVEPOINT \"{}\"", name_owned), &[]) + conn.execute(&format!("SAVEPOINT \"{}\"", name_owned), &[]) .await .map_err(|e| AegisError::StorageQuery(e.to_string()))?; Ok(()) @@ -1781,8 +1910,7 @@ impl StorageTransaction for PostgresTransaction { let name_owned = name.to_string(); self.block_on(async { let conn = self.conn.as_ref().unwrap(); - conn - .execute(&format!("ROLLBACK TO SAVEPOINT \"{}\"", name_owned), &[]) + conn.execute(&format!("ROLLBACK TO SAVEPOINT \"{}\"", name_owned), &[]) .await .map_err(|e| AegisError::StorageQuery(e.to_string()))?; Ok(()) @@ -1794,8 +1922,7 @@ impl StorageTransaction for PostgresTransaction { let name_owned = name.to_string(); self.block_on(async { let conn = self.conn.as_ref().unwrap(); - conn - .execute(&format!("RELEASE SAVEPOINT \"{}\"", name_owned), &[]) + conn.execute(&format!("RELEASE SAVEPOINT \"{}\"", name_owned), &[]) .await .map_err(|e| AegisError::StorageQuery(e.to_string()))?; Ok(()) @@ -1804,7 +1931,8 @@ impl StorageTransaction for PostgresTransaction { fn commit(self: Box) -> AegisResult { let s = *self; - let conn = s.conn + let conn = s + .conn .ok_or_else(|| AegisError::Internal("transaction already consumed".into()))?; let handle = s.runtime; handle.block_on(async { @@ -1826,12 +1954,12 @@ impl StorageTransaction for PostgresTransaction { fn rollback(self: Box) -> AegisResult<()> { let s = *self; - let conn = s.conn + let conn = s + .conn .ok_or_else(|| AegisError::Internal("transaction already consumed".into()))?; let handle = s.runtime; handle.block_on(async { - conn - .execute("ROLLBACK", &[]) + conn.execute("ROLLBACK", &[]) .await .map_err(|e| AegisError::StorageQuery(e.to_string()))?; Ok(()) diff --git a/crates/aegis-core/src/storage/rocksdb.rs b/crates/aegis-core/src/storage/rocksdb.rs index 3e8028e..55cc535 100644 --- a/crates/aegis-core/src/storage/rocksdb.rs +++ b/crates/aegis-core/src/storage/rocksdb.rs @@ -12,12 +12,9 @@ use crate::types::{ TupleMutation, }; use chrono::{DateTime, Utc}; -use rocksdb::{ - BlockBasedOptions, Cache, ColumnFamily, ColumnFamilyDescriptor, DBIterator, Direction, - IteratorMode, Options, DB, -}; -use serde_json; +use rocksdb::{BlockBasedOptions, Cache, DB, Direction, IteratorMode, Options}; use std::collections::HashMap; +use std::sync::Arc; use uuid::Uuid; const CF_META: &str = "meta"; @@ -34,11 +31,19 @@ const META_REVISION: &str = "revision"; const META_SCHEMA_VERSION: &str = "schema_version"; fn tuple_key(partition_id: &str, subject: &str, relation: &str, object: &str) -> Vec { - format!("{}\x00{}\x00{}\x00{}", partition_id, subject, relation, object).into_bytes() + format!( + "{}\x00{}\x00{}\x00{}", + partition_id, subject, relation, object + ) + .into_bytes() } fn object_idx_key(partition_id: &str, object: &str, relation: &str, subject: &str) -> Vec { - format!("{}\x00{}\x00{}\x00{}", partition_id, object, relation, subject).into_bytes() + format!( + "{}\x00{}\x00{}\x00{}", + partition_id, object, relation, subject + ) + .into_bytes() } fn event_key(partition_id: &str, revision: Revision, id: Uuid) -> Vec { @@ -46,14 +51,12 @@ fn event_key(partition_id: &str, revision: Revision, id: Uuid) -> Vec { } fn tuple_from_value(value: &[u8]) -> AegisResult { - let s = std::str::from_utf8(value) - .map_err(|e| AegisError::StorageQuery(e.to_string()))?; - serde_json::from_str(s) - .map_err(|e| AegisError::StorageQuery(e.to_string())) + let s = std::str::from_utf8(value).map_err(|e| AegisError::StorageQuery(e.to_string()))?; + serde_json::from_str(s).map_err(|e| AegisError::StorageQuery(e.to_string())) } pub struct RocksDbStorage { - db: DB, + db: Arc, node_id: Uuid, revision_mutex: std::sync::Mutex<()>, actor_identity: std::sync::Mutex>, @@ -66,7 +69,7 @@ impl RocksDbStorage { opts.create_if_missing(true); // Configure block cache (8 MiB per column family) - let cache = Cache::new(8 * 1024 * 1024); + let cache = Cache::new_lru_cache(8 * 1024 * 1024); let mut block_opts = BlockBasedOptions::default(); block_opts.set_block_cache(&cache); block_opts.set_block_size(4 * 1024); // 4 KiB blocks @@ -85,39 +88,47 @@ impl RocksDbStorage { CF_ENFORCEMENT_EVENTS, ]; - let db = DB::open_cf(&opts, path, cfs) - .map_err(|e| AegisError::StorageConnection(e.to_string()))?; + let db = Arc::new( + DB::open_cf(&opts, path, cfs) + .map_err(|e| AegisError::StorageConnection(e.to_string()))?, + ); // Initialize revision if not present - let cf_meta = db.cf_handle(CF_META) + let cf_meta = db + .cf_handle(CF_META) .ok_or_else(|| AegisError::StorageConnection("missing meta cf".into()))?; - if db.get_cf(&cf_meta, META_REVISION.as_bytes()) + if db + .get_cf(&cf_meta, META_REVISION.as_bytes()) .map_err(|e| AegisError::StorageQuery(e.to_string()))? .is_none() { - db.put_cf(&cf_meta, META_REVISION.as_bytes(), &0u64.to_le_bytes()) + db.put_cf(&cf_meta, META_REVISION.as_bytes(), 0u64.to_le_bytes()) .map_err(|e| AegisError::StorageQuery(e.to_string()))?; } - if db.get_cf(&cf_meta, META_SCHEMA_VERSION.as_bytes()) + if db + .get_cf(&cf_meta, META_SCHEMA_VERSION.as_bytes()) .map_err(|e| AegisError::StorageQuery(e.to_string()))? .is_none() { - db.put_cf(&cf_meta, META_SCHEMA_VERSION.as_bytes(), &1u32.to_le_bytes()) + db.put_cf(&cf_meta, META_SCHEMA_VERSION.as_bytes(), 1u32.to_le_bytes()) .map_err(|e| AegisError::StorageQuery(e.to_string()))?; } Ok(Self { db, - node_id, + node_id: Uuid::new_v4(), revision_mutex: std::sync::Mutex::new(()), actor_identity: std::sync::Mutex::new(None), }) } + #[allow(dead_code)] fn read_schema_version(&self) -> AegisResult { - let cf = self.db.cf_handle(CF_META) + let cf = self + .db + .cf_handle(CF_META) .ok_or_else(|| AegisError::StorageConnection("missing meta cf".into()))?; match self.db.get_cf(&cf, META_SCHEMA_VERSION.as_bytes()) { Ok(Some(val)) if val.len() >= 4 => { @@ -129,45 +140,65 @@ impl RocksDbStorage { } } + #[allow(dead_code)] fn write_schema_version(&self, version: u32) -> AegisResult<()> { - let cf = self.db.cf_handle(CF_META) + let cf = self + .db + .cf_handle(CF_META) .ok_or_else(|| AegisError::StorageConnection("missing meta cf".into()))?; - self.db.put_cf(&cf, META_SCHEMA_VERSION.as_bytes(), &version.to_le_bytes()) + self.db + .put_cf(&cf, META_SCHEMA_VERSION.as_bytes(), version.to_le_bytes()) .map_err(|e| AegisError::StorageQuery(e.to_string())) } fn read_revision(&self) -> AegisResult { - let cf = self.db.cf_handle(CF_META) + let cf = self + .db + .cf_handle(CF_META) .ok_or_else(|| AegisError::StorageConnection("missing meta cf".into()))?; - let val = self.db.get_cf(&cf, META_REVISION.as_bytes()) + let val = self + .db + .get_cf(&cf, META_REVISION.as_bytes()) .map_err(|e| AegisError::StorageQuery(e.to_string()))? .unwrap_or_else(|| 0u64.to_le_bytes().to_vec()); let rev = u64::from_le_bytes( - val.try_into().map_err(|_| AegisError::StorageQuery("invalid revision".into()))?, + val.try_into() + .map_err(|_| AegisError::StorageQuery("invalid revision".into()))?, ); Ok(Revision::new(rev)) } fn bump_revision(&self) -> AegisResult { - let _guard = self.revision_mutex.lock() + let _guard = self + .revision_mutex + .lock() .map_err(|_| AegisError::Internal("revision mutex poisoned".into()))?; - let cf = self.db.cf_handle(CF_META) + let cf = self + .db + .cf_handle(CF_META) .ok_or_else(|| AegisError::StorageConnection("missing meta cf".into()))?; - let val = self.db.get_cf(&cf, META_REVISION.as_bytes()) + let val = self + .db + .get_cf(&cf, META_REVISION.as_bytes()) .map_err(|e| AegisError::StorageQuery(e.to_string()))? .unwrap_or_else(|| 0u64.to_le_bytes().to_vec()); let rev = u64::from_le_bytes( - val.try_into().map_err(|_| AegisError::StorageQuery("invalid revision".into()))?, + val.try_into() + .map_err(|_| AegisError::StorageQuery("invalid revision".into()))?, ); - let new_rev = rev.checked_add(1) + let new_rev = rev + .checked_add(1) .ok_or_else(|| AegisError::Internal("revision overflow".into()))?; - self.db.put_cf(&cf, META_REVISION.as_bytes(), &new_rev.to_le_bytes()) + self.db + .put_cf(&cf, META_REVISION.as_bytes(), new_rev.to_le_bytes()) .map_err(|e| AegisError::StorageQuery(e.to_string()))?; Ok(Revision::new(new_rev)) } fn last_event_hash(&self, partition_id: &PartitionId) -> AegisResult { - let cf = self.db.cf_handle(CF_EVENTS) + let cf = self + .db + .cf_handle(CF_EVENTS) .ok_or_else(|| AegisError::StorageConnection("missing events cf".into()))?; let pid_prefix = format!("{}:", partition_id.as_str()).into_bytes(); // Seek to the end of the partition's event range @@ -178,6 +209,7 @@ impl RocksDbStorage { if !key.starts_with(&pid_prefix) { break; } + #[allow(clippy::collapsible_if)] if let Ok(event) = serde_json::from_slice::(&value) { if let Some(h) = event["event_hash"].as_str() { last_hash = h.to_string(); @@ -187,6 +219,7 @@ impl RocksDbStorage { Ok(last_hash) } + #[allow(clippy::too_many_arguments)] fn append_event( &self, partition_id: &PartitionId, @@ -198,7 +231,9 @@ impl RocksDbStorage { metadata: Option<&str>, identity: Option<&str>, ) -> AegisResult<()> { - let cf = self.db.cf_handle(CF_EVENTS) + let cf = self + .db + .cf_handle(CF_EVENTS) .ok_or_else(|| AegisError::StorageConnection("missing events cf".into()))?; let now = Utc::now().to_rfc3339(); let previous_hash = self.last_event_hash(partition_id)?; @@ -228,45 +263,86 @@ impl RocksDbStorage { }); let event_id = Uuid::new_v4(); let key = event_key(partition_id.as_str(), revision, event_id); - let val = serde_json::to_string(&event) - .map_err(|e| AegisError::StorageQuery(e.to_string()))?; - self.db.put_cf(&cf, key, val.as_bytes()) + let val = + serde_json::to_string(&event).map_err(|e| AegisError::StorageQuery(e.to_string()))?; + self.db + .put_cf(&cf, key, val.as_bytes()) .map_err(|e| AegisError::StorageQuery(e.to_string()))?; Ok(()) } - fn put_tuple(&self, partition_id: &PartitionId, tuple: &RelationshipTuple, revision: Revision) -> AegisResult<()> { - let cf_tuples = self.db.cf_handle(CF_TUPLES) + fn put_tuple( + &self, + partition_id: &PartitionId, + tuple: &RelationshipTuple, + _revision: Revision, + ) -> AegisResult<()> { + let cf_tuples = self + .db + .cf_handle(CF_TUPLES) .ok_or_else(|| AegisError::StorageConnection("missing tuples cf".into()))?; - let cf_idx = self.db.cf_handle(CF_IDX_OBJECT) + let cf_idx = self + .db + .cf_handle(CF_IDX_OBJECT) .ok_or_else(|| AegisError::StorageConnection("missing idx_object cf".into()))?; - let val = serde_json::to_string(tuple) - .map_err(|e| AegisError::StorageQuery(e.to_string()))?; - let key = tuple_key(partition_id.as_str(), tuple.subject.as_str(), tuple.relation.as_str(), tuple.object.as_str()); - let idx_key = object_idx_key(partition_id.as_str(), tuple.object.as_str(), tuple.relation.as_str(), tuple.subject.as_str()); + let val = + serde_json::to_string(tuple).map_err(|e| AegisError::StorageQuery(e.to_string()))?; + let key = tuple_key( + partition_id.as_str(), + tuple.subject.as_str(), + tuple.relation.as_str(), + tuple.object.as_str(), + ); + let idx_key = object_idx_key( + partition_id.as_str(), + tuple.object.as_str(), + tuple.relation.as_str(), + tuple.subject.as_str(), + ); let mut batch = rocksdb::WriteBatch::default(); batch.put_cf(&cf_tuples, &key, val.as_bytes()); - batch.put_cf(&cf_idx, &idx_key, &[]); - self.db.write(batch) + batch.put_cf(&cf_idx, &idx_key, []); + self.db + .write(batch) .map_err(|e| AegisError::StorageQuery(e.to_string()))?; Ok(()) } - fn delete_tuple_key(&self, partition_id: &PartitionId, key: &TupleKey, revision: Revision) -> AegisResult<()> { - let cf_tuples = self.db.cf_handle(CF_TUPLES) + fn delete_tuple_key( + &self, + partition_id: &PartitionId, + key: &TupleKey, + _revision: Revision, + ) -> AegisResult<()> { + let cf_tuples = self + .db + .cf_handle(CF_TUPLES) .ok_or_else(|| AegisError::StorageConnection("missing tuples cf".into()))?; - let cf_idx = self.db.cf_handle(CF_IDX_OBJECT) + let cf_idx = self + .db + .cf_handle(CF_IDX_OBJECT) .ok_or_else(|| AegisError::StorageConnection("missing idx_object cf".into()))?; - let pk = tuple_key(partition_id.as_str(), key.subject.as_str(), key.relation.as_str(), key.object.as_str()); - let idx_key = object_idx_key(partition_id.as_str(), key.object.as_str(), key.relation.as_str(), key.subject.as_str()); + let pk = tuple_key( + partition_id.as_str(), + key.subject.as_str(), + key.relation.as_str(), + key.object.as_str(), + ); + let idx_key = object_idx_key( + partition_id.as_str(), + key.object.as_str(), + key.relation.as_str(), + key.subject.as_str(), + ); let mut batch = rocksdb::WriteBatch::default(); batch.delete_cf(&cf_tuples, &pk); batch.delete_cf(&cf_idx, &idx_key); - self.db.write(batch) + self.db + .write(batch) .map_err(|e| AegisError::StorageQuery(e.to_string()))?; Ok(()) } @@ -294,7 +370,11 @@ impl StorageBackend for RocksDbStorage { }) } - fn write_tuple(&self, partition_id: &PartitionId, tuple: &RelationshipTuple) -> AegisResult { + fn write_tuple( + &self, + partition_id: &PartitionId, + tuple: &RelationshipTuple, + ) -> AegisResult { let revision = self.bump_revision()?; // Remove existing tuple if present @@ -305,7 +385,9 @@ impl StorageBackend for RocksDbStorage { self.put_tuple(partition_id, tuple, revision)?; - let metadata_json = tuple.metadata.as_ref() + let metadata_json = tuple + .metadata + .as_ref() .map(serde_json::to_string) .transpose() .map_err(|e| AegisError::MetadataValidation(e.to_string()))?; @@ -325,26 +407,46 @@ impl StorageBackend for RocksDbStorage { Ok(revision) } - fn write_tuples_batch(&self, partition_id: &PartitionId, tuples: &[RelationshipTuple]) -> AegisResult { + fn write_tuples_batch( + &self, + partition_id: &PartitionId, + tuples: &[RelationshipTuple], + ) -> AegisResult { if tuples.is_empty() { return self.current_revision(partition_id); } let revision = self.bump_revision()?; - let cf_tuples = self.db.cf_handle(CF_TUPLES) + let cf_tuples = self + .db + .cf_handle(CF_TUPLES) .ok_or_else(|| AegisError::StorageConnection("missing tuples cf".into()))?; - let cf_idx = self.db.cf_handle(CF_IDX_OBJECT) + let cf_idx = self + .db + .cf_handle(CF_IDX_OBJECT) .ok_or_else(|| AegisError::StorageConnection("missing idx_object cf".into()))?; - let cf_events = self.db.cf_handle(CF_EVENTS) + let cf_events = self + .db + .cf_handle(CF_EVENTS) .ok_or_else(|| AegisError::StorageConnection("missing events cf".into()))?; let mut batch = rocksdb::WriteBatch::default(); let now = Utc::now().to_rfc3339(); let mut previous_hash = self.last_event_hash(partition_id)?; for tuple in tuples { - let pk = tuple_key(partition_id.as_str(), tuple.subject.as_str(), tuple.relation.as_str(), tuple.object.as_str()); - let idx_key = object_idx_key(partition_id.as_str(), tuple.object.as_str(), tuple.relation.as_str(), tuple.subject.as_str()); + let pk = tuple_key( + partition_id.as_str(), + tuple.subject.as_str(), + tuple.relation.as_str(), + tuple.object.as_str(), + ); + let idx_key = object_idx_key( + partition_id.as_str(), + tuple.object.as_str(), + tuple.relation.as_str(), + tuple.subject.as_str(), + ); let val = serde_json::to_string(tuple) .map_err(|e| AegisError::StorageQuery(e.to_string()))?; @@ -353,9 +455,11 @@ impl StorageBackend for RocksDbStorage { batch.delete_cf(&cf_idx, &idx_key); // Insert new batch.put_cf(&cf_tuples, &pk, val.as_bytes()); - batch.put_cf(&cf_idx, &idx_key, &[]); + batch.put_cf(&cf_idx, &idx_key, []); - let metadata_str = tuple.metadata.as_ref() + let metadata_str = tuple + .metadata + .as_ref() .map(serde_json::to_string) .transpose() .map_err(|e| AegisError::MetadataValidation(e.to_string()))?; @@ -386,9 +490,16 @@ impl StorageBackend for RocksDbStorage { }); previous_hash = event_hash; let event_id = Uuid::new_v4(); - batch.put_cf(&cf_events, event_key(partition_id.as_str(), revision, event_id), serde_json::to_string(&event).map_err(|e| AegisError::StorageQuery(e.to_string()))?.as_bytes()); + batch.put_cf( + &cf_events, + event_key(partition_id.as_str(), revision, event_id), + serde_json::to_string(&event) + .map_err(|e| AegisError::StorageQuery(e.to_string()))? + .as_bytes(), + ); } - self.db.write(batch) + self.db + .write(batch) .map_err(|e| AegisError::StorageQuery(e.to_string()))?; Ok(revision) @@ -418,19 +529,34 @@ impl StorageBackend for RocksDbStorage { Ok(revision) } - fn delete_subject(&self, partition_id: &PartitionId, subject: &SubjectId) -> AegisResult { - let tuples = self.list_by_subject(partition_id, subject, None, &ConsistencyMode::MinimizeLatency)?; + fn delete_subject( + &self, + partition_id: &PartitionId, + subject: &SubjectId, + ) -> AegisResult { + let tuples = self.list_by_subject( + partition_id, + subject, + None, + &ConsistencyMode::MinimizeLatency, + )?; if tuples.is_empty() { return self.current_revision(partition_id); } let revision = self.bump_revision()?; - let cf_tuples = self.db.cf_handle(CF_TUPLES) + let cf_tuples = self + .db + .cf_handle(CF_TUPLES) .ok_or_else(|| AegisError::StorageConnection("missing tuples cf".into()))?; - let cf_idx = self.db.cf_handle(CF_IDX_OBJECT) + let cf_idx = self + .db + .cf_handle(CF_IDX_OBJECT) .ok_or_else(|| AegisError::StorageConnection("missing idx_object cf".into()))?; - let cf_events = self.db.cf_handle(CF_EVENTS) + let cf_events = self + .db + .cf_handle(CF_EVENTS) .ok_or_else(|| AegisError::StorageConnection("missing events cf".into()))?; let prefix = format!("{}\x00{}\x00", partition_id.as_str(), subject.as_str()).into_bytes(); @@ -498,28 +624,48 @@ impl StorageBackend for RocksDbStorage { let json_bytes = serde_json::to_string(&event) .map_err(|e| AegisError::StorageQuery(e.to_string()))? .into_bytes(); - batch.put_cf(&cf_events, event_key(partition_id.as_str(), revision, event_id), &json_bytes); + batch.put_cf( + &cf_events, + event_key(partition_id.as_str(), revision, event_id), + &json_bytes, + ); } - self.db.write(batch) + self.db + .write(batch) .map_err(|e| AegisError::StorageQuery(e.to_string()))?; Ok(revision) } - fn delete_object(&self, partition_id: &PartitionId, object: &ResourceId) -> AegisResult { - let tuples = self.list_by_object(partition_id, object, None, &ConsistencyMode::MinimizeLatency)?; + fn delete_object( + &self, + partition_id: &PartitionId, + object: &ResourceId, + ) -> AegisResult { + let tuples = self.list_by_object( + partition_id, + object, + None, + &ConsistencyMode::MinimizeLatency, + )?; if tuples.is_empty() { return self.current_revision(partition_id); } let revision = self.bump_revision()?; - let cf_tuples = self.db.cf_handle(CF_TUPLES) + let cf_tuples = self + .db + .cf_handle(CF_TUPLES) .ok_or_else(|| AegisError::StorageConnection("missing tuples cf".into()))?; - let cf_idx = self.db.cf_handle(CF_IDX_OBJECT) + let cf_idx = self + .db + .cf_handle(CF_IDX_OBJECT) .ok_or_else(|| AegisError::StorageConnection("missing idx_object cf".into()))?; - let cf_events = self.db.cf_handle(CF_EVENTS) + let cf_events = self + .db + .cf_handle(CF_EVENTS) .ok_or_else(|| AegisError::StorageConnection("missing events cf".into()))?; let mut batch = rocksdb::WriteBatch::default(); @@ -528,8 +674,18 @@ impl StorageBackend for RocksDbStorage { let mut previous_hash = self.last_event_hash(partition_id)?; let identity = self.actor_identity.lock().unwrap().clone(); for tuple in &tuples { - let pk = tuple_key(partition_id.as_str(), tuple.subject.as_str(), tuple.relation.as_str(), tuple.object.as_str()); - let idx_key = object_idx_key(partition_id.as_str(), tuple.object.as_str(), tuple.relation.as_str(), tuple.subject.as_str()); + let pk = tuple_key( + partition_id.as_str(), + tuple.subject.as_str(), + tuple.relation.as_str(), + tuple.object.as_str(), + ); + let idx_key = object_idx_key( + partition_id.as_str(), + tuple.object.as_str(), + tuple.relation.as_str(), + tuple.subject.as_str(), + ); batch.delete_cf(&cf_tuples, &pk); batch.delete_cf(&cf_idx, &idx_key); @@ -562,29 +718,56 @@ impl StorageBackend for RocksDbStorage { let json_bytes = serde_json::to_string(&event) .map_err(|e| AegisError::StorageQuery(e.to_string()))? .into_bytes(); - batch.put_cf(&cf_events, event_key(partition_id.as_str(), revision, event_id), &json_bytes); + batch.put_cf( + &cf_events, + event_key(partition_id.as_str(), revision, event_id), + &json_bytes, + ); } - self.db.write(batch) + self.db + .write(batch) .map_err(|e| AegisError::StorageQuery(e.to_string()))?; Ok(revision) } fn has_tuple(&self, partition_id: &PartitionId, key: &TupleKey) -> AegisResult { - let cf = self.db.cf_handle(CF_TUPLES) + let cf = self + .db + .cf_handle(CF_TUPLES) .ok_or_else(|| AegisError::StorageConnection("missing tuples cf".into()))?; - let pk = tuple_key(partition_id.as_str(), key.subject.as_str(), key.relation.as_str(), key.object.as_str()); - let val = self.db.get_cf(&cf, &pk) + let pk = tuple_key( + partition_id.as_str(), + key.subject.as_str(), + key.relation.as_str(), + key.object.as_str(), + ); + let val = self + .db + .get_cf(&cf, &pk) .map_err(|e| AegisError::StorageQuery(e.to_string()))?; Ok(val.is_some()) } - fn read_tuple(&self, partition_id: &PartitionId, key: &TupleKey) -> AegisResult> { - let cf = self.db.cf_handle(CF_TUPLES) + fn read_tuple( + &self, + partition_id: &PartitionId, + key: &TupleKey, + ) -> AegisResult> { + let cf = self + .db + .cf_handle(CF_TUPLES) .ok_or_else(|| AegisError::StorageConnection("missing tuples cf".into()))?; - let pk = tuple_key(partition_id.as_str(), key.subject.as_str(), key.relation.as_str(), key.object.as_str()); - let val = self.db.get_cf(&cf, &pk) + let pk = tuple_key( + partition_id.as_str(), + key.subject.as_str(), + key.relation.as_str(), + key.object.as_str(), + ); + let val = self + .db + .get_cf(&cf, &pk) .map_err(|e| AegisError::StorageQuery(e.to_string()))?; match val { Some(v) => Ok(Some(tuple_from_value(&v)?)), @@ -599,10 +782,18 @@ impl StorageBackend for RocksDbStorage { relation: Option<&Relation>, consistency: &ConsistencyMode, ) -> AegisResult> { - let cf = self.db.cf_handle(CF_TUPLES) + let cf = self + .db + .cf_handle(CF_TUPLES) .ok_or_else(|| AegisError::StorageConnection("missing tuples cf".into()))?; let prefix = match relation { - Some(rel) => format!("{}\x00{}\x00{}\x00", partition_id.as_str(), object.as_str(), rel.as_str()).into_bytes(), + Some(rel) => format!( + "{}\x00{}\x00{}\x00", + partition_id.as_str(), + object.as_str(), + rel.as_str() + ) + .into_bytes(), None => format!("{}\x00{}\x00", partition_id.as_str(), object.as_str()).into_bytes(), }; @@ -614,11 +805,13 @@ impl StorageBackend for RocksDbStorage { }; // We need to find tuples by object: scan idx_object first, then fetch from tuples - let cf_idx = self.db.cf_handle(CF_IDX_OBJECT) + let cf_idx = self + .db + .cf_handle(CF_IDX_OBJECT) .ok_or_else(|| AegisError::StorageConnection("missing idx_object cf".into()))?; let iter = if let Some(ref snap) = snapshot { - snap.prefix_iterator_cf(&cf_idx, &prefix) + snap.iterator_cf(&cf_idx, IteratorMode::From(&prefix, Direction::Forward)) } else { self.db.prefix_iterator_cf(&cf_idx, &prefix) }; @@ -641,6 +834,7 @@ impl StorageBackend for RocksDbStorage { } else { self.db.get_cf(&cf, &pk) }; + #[allow(clippy::collapsible_if)] if let Some(val) = val.map_err(|e| AegisError::StorageQuery(e.to_string()))? { if let Ok(tuple) = tuple_from_value(&val) { results.push(tuple); @@ -659,10 +853,18 @@ impl StorageBackend for RocksDbStorage { relation: Option<&Relation>, consistency: &ConsistencyMode, ) -> AegisResult> { - let cf = self.db.cf_handle(CF_TUPLES) + let cf = self + .db + .cf_handle(CF_TUPLES) .ok_or_else(|| AegisError::StorageConnection("missing tuples cf".into()))?; let prefix = match relation { - Some(rel) => format!("{}\x00{}\x00{}\x00", partition_id.as_str(), subject.as_str(), rel.as_str()).into_bytes(), + Some(rel) => format!( + "{}\x00{}\x00{}\x00", + partition_id.as_str(), + subject.as_str(), + rel.as_str() + ) + .into_bytes(), None => format!("{}\x00{}\x00", partition_id.as_str(), subject.as_str()).into_bytes(), }; @@ -673,7 +875,7 @@ impl StorageBackend for RocksDbStorage { }; let iter = if let Some(ref snap) = snapshot { - snap.prefix_iterator_cf(&cf, &prefix) + snap.iterator_cf(&cf, IteratorMode::From(&prefix, Direction::Forward)) } else { self.db.prefix_iterator_cf(&cf, &prefix) }; @@ -697,7 +899,12 @@ impl StorageBackend for RocksDbStorage { object: &ResourceId, relation: &Relation, ) -> AegisResult> { - self.list_by_object(partition_id, object, Some(relation), &ConsistencyMode::MinimizeLatency) + self.list_by_object( + partition_id, + object, + Some(relation), + &ConsistencyMode::MinimizeLatency, + ) } fn query_tuples( @@ -716,9 +923,13 @@ impl StorageBackend for RocksDbStorage { None }; - let cf_tuples = self.db.cf_handle(CF_TUPLES) + let cf_tuples = self + .db + .cf_handle(CF_TUPLES) .ok_or_else(|| AegisError::StorageConnection("missing tuples cf".into()))?; - let cf_idx = self.db.cf_handle(CF_IDX_OBJECT) + let cf_idx = self + .db + .cf_handle(CF_IDX_OBJECT) .ok_or_else(|| AegisError::StorageConnection("missing idx_object cf".into()))?; let offset = pagination.cursor.as_ref().map(|c| c.offset).unwrap_or(0); @@ -732,12 +943,17 @@ impl StorageBackend for RocksDbStorage { // Use the object index for efficient filtering let pid_prefix_bytes = pid_prefix.as_bytes(); let iter: Box>> = if let Some(ref snap) = snapshot { - Box::new(snap.prefix_iterator_cf(&cf_idx, pid_prefix_bytes)) + Box::new(snap.iterator_cf( + &cf_idx, + IteratorMode::From(pid_prefix_bytes, Direction::Forward), + )) } else { Box::new(self.db.prefix_iterator_cf(&cf_idx, pid_prefix_bytes)) }; for item in iter { - if all_tuples.len() >= limit { break; } + if all_tuples.len() >= limit { + break; + } let (key, _) = item.map_err(|e| AegisError::StorageQuery(e.to_string()))?; if !key.starts_with(pid_prefix_bytes) { break; @@ -745,19 +961,30 @@ impl StorageBackend for RocksDbStorage { let key_str = std::str::from_utf8(&key) .map_err(|e| AegisError::StorageQuery(e.to_string()))?; let parts: Vec<&str> = key_str.split('\x00').collect(); - if parts.len() < 4 { continue; } + if parts.len() < 4 { + continue; + } let obj = parts[1]; let rel = parts[2]; let subj = parts[3]; + #[allow(clippy::collapsible_if)] if let Some(ref ot) = filter.object_type { - if !obj.starts_with(&format!("{ot}:")) { continue; } + if !obj.starts_with(&format!("{ot}:")) { + continue; + } } + #[allow(clippy::collapsible_if)] if let Some(ref r) = filter.relation { - if rel != r.as_str() { continue; } + if rel != r.as_str() { + continue; + } } + #[allow(clippy::collapsible_if)] if let Some(ref st) = filter.subject_type { - if !subj.starts_with(&format!("{st}:")) { continue; } + if !subj.starts_with(&format!("{st}:")) { + continue; + } } let pk = tuple_key(partition_id.as_str(), subj, rel, obj); @@ -766,13 +993,19 @@ impl StorageBackend for RocksDbStorage { } else { self.db.get_cf(&cf_tuples, &pk) }; + #[allow(clippy::collapsible_if)] if let Ok(Some(value)) = value_opt { + #[allow(clippy::collapsible_if)] if let Ok(tuple) = tuple_from_value(&value) { if let Some(ref mk) = filter.metadata_key { - let has_key = tuple.metadata.as_ref() + let has_key = tuple + .metadata + .as_ref() .map(|m| m.contains_key(mk)) .unwrap_or(false); - if !has_key { continue; } + if !has_key { + continue; + } } all_tuples.push(tuple); } @@ -782,21 +1015,29 @@ impl StorageBackend for RocksDbStorage { // Prefix scan by subject type within partition let prefix = format!("{}\x00{}:", partition_id.as_str(), st); let prefix_bytes = prefix.as_bytes(); - let iter: Box, Box<[u8]>), _>>> = if let Some(ref snap) = snapshot { - Box::new(snap.iterator_cf(&cf_tuples, IteratorMode::From( - prefix_bytes, Direction::Forward, - ))) - } else { - Box::new(self.db.iterator_cf(&cf_tuples, IteratorMode::From( - prefix_bytes, Direction::Forward, - ))) - }; + #[allow(clippy::type_complexity)] + let iter: Box, Box<[u8]>), _>>> = + if let Some(ref snap) = snapshot { + Box::new(snap.iterator_cf( + &cf_tuples, + IteratorMode::From(prefix_bytes, Direction::Forward), + )) + } else { + Box::new(self.db.iterator_cf( + &cf_tuples, + IteratorMode::From(prefix_bytes, Direction::Forward), + )) + }; for item in iter { - if all_tuples.len() >= limit { break; } + if all_tuples.len() >= limit { + break; + } let (key, value) = item.map_err(|e| AegisError::StorageQuery(e.to_string()))?; let key_str = std::str::from_utf8(&key) .map_err(|e| AegisError::StorageQuery(e.to_string()))?; - if !key_str.starts_with(&prefix) { break; } + if !key_str.starts_with(&prefix) { + break; + } if let Ok(tuple) = tuple_from_value(&value) { all_tuples.push(tuple); } @@ -804,29 +1045,45 @@ impl StorageBackend for RocksDbStorage { } else if filter.metadata_key.is_some() || filter.metadata_value.is_some() { // Full scan within partition for metadata filtering, but bounded by limit let pid_prefix_bytes = pid_prefix.as_bytes(); - let iter: Box, Box<[u8]>), _>>> = if let Some(ref snap) = snapshot { - Box::new(snap.prefix_iterator_cf(&cf_tuples, pid_prefix_bytes)) - } else { - Box::new(self.db.prefix_iterator_cf(&cf_tuples, pid_prefix_bytes)) - }; + #[allow(clippy::type_complexity)] + let iter: Box, Box<[u8]>), _>>> = + if let Some(ref snap) = snapshot { + Box::new(snap.iterator_cf( + &cf_tuples, + IteratorMode::From(pid_prefix_bytes, Direction::Forward), + )) + } else { + Box::new(self.db.prefix_iterator_cf(&cf_tuples, pid_prefix_bytes)) + }; for item in iter { - if all_tuples.len() >= limit { break; } + if all_tuples.len() >= limit { + break; + } let (key, value) = item.map_err(|e| AegisError::StorageQuery(e.to_string()))?; if !key.starts_with(pid_prefix_bytes) { break; } + #[allow(clippy::collapsible_if)] if let Ok(tuple) = tuple_from_value(&value) { if let Some(ref mk) = filter.metadata_key { - let has_key = tuple.metadata.as_ref() + let has_key = tuple + .metadata + .as_ref() .map(|m| m.contains_key(mk)) .unwrap_or(false); - if !has_key { continue; } + if !has_key { + continue; + } } if let Some(ref mv) = filter.metadata_value { - let has_val = tuple.metadata.as_ref() + let has_val = tuple + .metadata + .as_ref() .and_then(|m| m.values().find(|v| *v == mv)) .is_some(); - if !has_val { continue; } + if !has_val { + continue; + } } all_tuples.push(tuple); } @@ -849,14 +1106,18 @@ impl StorageBackend for RocksDbStorage { let next_cursor = if (offset as usize + tuples.len()) < total { Some(PaginationCursor { - offset: offset + limit, + offset: offset + limit as u64, revision, }) } else { None }; - Ok(PaginatedTuples { tuples, next_cursor, revision }) + Ok(PaginatedTuples { + tuples, + next_cursor, + revision, + }) } fn current_revision(&self, _partition_id: &PartitionId) -> AegisResult { @@ -864,19 +1125,42 @@ impl StorageBackend for RocksDbStorage { } fn current_token(&self) -> AegisResult { - let revision = self.current_revision()?; + let revision = self.current_revision(&PartitionId::default())?; Ok(RevisionToken::new(revision, self.node_id)) } - fn begin_transaction(&self, partition_id: &PartitionId) -> AegisResult> { - let cf_tuples = self.db.cf_handle(CF_TUPLES) - .ok_or_else(|| AegisError::StorageConnection("missing tuples cf".into()))?; - let cf_idx = self.db.cf_handle(CF_IDX_OBJECT) - .ok_or_else(|| AegisError::StorageConnection("missing idx_object cf".into()))?; - let cf_events = self.db.cf_handle(CF_EVENTS) - .ok_or_else(|| AegisError::StorageConnection("missing events cf".into()))?; - let cf_meta = self.db.cf_handle(CF_META) - .ok_or_else(|| AegisError::StorageConnection("missing meta cf".into()))?; + fn begin_transaction( + &self, + partition_id: &PartitionId, + ) -> AegisResult> { + let cf_tuples: &'static rocksdb::ColumnFamily = unsafe { + &*(self + .db + .cf_handle(CF_TUPLES) + .ok_or_else(|| AegisError::StorageConnection("missing tuples cf".into()))? + as *const rocksdb::ColumnFamily) + }; + let cf_idx: &'static rocksdb::ColumnFamily = unsafe { + &*(self + .db + .cf_handle(CF_IDX_OBJECT) + .ok_or_else(|| AegisError::StorageConnection("missing idx_object cf".into()))? + as *const rocksdb::ColumnFamily) + }; + let cf_events: &'static rocksdb::ColumnFamily = unsafe { + &*(self + .db + .cf_handle(CF_EVENTS) + .ok_or_else(|| AegisError::StorageConnection("missing events cf".into()))? + as *const rocksdb::ColumnFamily) + }; + let cf_meta: &'static rocksdb::ColumnFamily = unsafe { + &*(self + .db + .cf_handle(CF_META) + .ok_or_else(|| AegisError::StorageConnection("missing meta cf".into()))? + as *const rocksdb::ColumnFamily) + }; let identity = self.actor_identity.lock().unwrap().clone(); Ok(Box::new(RocksDbTransaction { @@ -902,10 +1186,11 @@ impl StorageBackend for RocksDbStorage { to_revision: Option, pagination: &PaginationParams, ) -> AegisResult> { - let cf_events = self.db.cf_handle(CF_EVENTS) + let cf_events = self + .db + .cf_handle(CF_EVENTS) .ok_or_else(|| AegisError::StorageConnection("missing events cf".into()))?; - let offset = pagination.cursor.as_ref().map(|c| c.offset).unwrap_or(0); let limit = pagination.limit as usize; let from_rev = from_revision.map(|r| r.as_u64()); @@ -941,21 +1226,31 @@ impl StorageBackend for RocksDbStorage { let key_pid_len = pid_prefix_bytes.len(); let rev_part = &key[key_pid_len..]; if let Some(to) = to_rev { - if rev_part.len() < 16 { break; } - let key_rev = u64::from_str_radix( - std::str::from_utf8(&rev_part[..16]).unwrap_or(""), - 16, - ).unwrap_or(0); - if key_rev > to { break; } + if rev_part.len() < 16 { + break; + } + let key_rev = + u64::from_str_radix(std::str::from_utf8(&rev_part[..16]).unwrap_or(""), 16) + .unwrap_or(0); + if key_rev > to { + break; + } } if let Ok(event) = serde_json::from_slice::(&value) { let rev = event["revision"].as_u64().unwrap_or(0); - if from_rev.map(|f| rev < f).unwrap_or(false) { continue; } - if to_rev.map(|t| rev > t).unwrap_or(false) { break; } + if from_rev.map(|f| rev < f).unwrap_or(false) { + continue; + } + if to_rev.map(|t| rev > t).unwrap_or(false) { + break; + } + let event_obj = event["object"].as_str().unwrap_or("").to_string(); + #[allow(clippy::collapsible_if)] if let Some(obj) = object { - let event_obj = event["object"].as_str().unwrap_or(""); - if event_obj != obj.as_str() { continue; } + if event_obj != obj.as_str() { + continue; + } } let action = if event["action"] == "add" { @@ -966,10 +1261,12 @@ impl StorageBackend for RocksDbStorage { let ts_str = event["timestamp"].as_str().unwrap_or(""); let timestamp: DateTime = ts_str.parse().unwrap_or_else(|_| Utc::now()); - let metadata: Option> = event.get("metadata") + let metadata: Option> = event + .get("metadata") .and_then(|m| serde_json::from_value(m.clone()).ok()); - let identity: Option = event.get("identity") + let identity: Option = event + .get("identity") .and_then(|v| v.as_str()) .filter(|s| !s.is_empty()) .map(|s| s.to_string()); @@ -979,7 +1276,7 @@ impl StorageBackend for RocksDbStorage { action, subject: event["subject"].as_str().unwrap_or("").to_string(), relation: event["relation"].as_str().unwrap_or("").to_string(), - object: event_obj.to_string(), + object: event_obj, timestamp, metadata, identity, @@ -1001,7 +1298,7 @@ impl StorageBackend for RocksDbStorage { tenant_leakage_detected: false, leaked_crossings: vec![], orphaned_tuple_count: 0, - }) + }); } }; match self.db.get_cf(&cf_meta, META_REVISION.as_bytes()) { @@ -1033,7 +1330,9 @@ impl StorageBackend for RocksDbStorage { } fn read_schema_version(&self) -> AegisResult { - let cf = self.db.cf_handle(CF_META) + let cf = self + .db + .cf_handle(CF_META) .ok_or_else(|| AegisError::StorageConnection("missing meta cf".into()))?; match self.db.get_cf(&cf, META_SCHEMA_VERSION.as_bytes()) { Ok(Some(val)) if val.len() >= 4 => { @@ -1046,14 +1345,23 @@ impl StorageBackend for RocksDbStorage { } fn write_schema_version(&self, version: u32) -> AegisResult<()> { - let cf = self.db.cf_handle(CF_META) + let cf = self + .db + .cf_handle(CF_META) .ok_or_else(|| AegisError::StorageConnection("missing meta cf".into()))?; - self.db.put_cf(&cf, META_SCHEMA_VERSION.as_bytes(), &version.to_le_bytes()) + self.db + .put_cf(&cf, META_SCHEMA_VERSION.as_bytes(), version.to_le_bytes()) .map_err(|e| AegisError::StorageQuery(e.to_string())) } - fn delete_events_before(&self, partition_id: &PartitionId, cutoff: DateTime) -> AegisResult { - let cf = self.db.cf_handle(CF_EVENTS) + fn delete_events_before( + &self, + partition_id: &PartitionId, + cutoff: DateTime, + ) -> AegisResult { + let cf = self + .db + .cf_handle(CF_EVENTS) .ok_or_else(|| AegisError::StorageConnection("missing events cf".into()))?; let pid_prefix = format!("{}:", partition_id.as_str()).into_bytes(); let iter = self.db.prefix_iterator_cf(&cf, &pid_prefix); @@ -1065,6 +1373,7 @@ impl StorageBackend for RocksDbStorage { } if let Ok(event) = serde_json::from_slice::(&value) { let ts_str = event["timestamp"].as_str().unwrap_or(""); + #[allow(clippy::collapsible_if)] if let Ok(ts) = ts_str.parse::>() { if ts < cutoff { to_delete.push(key.to_vec()); @@ -1079,28 +1388,47 @@ impl StorageBackend for RocksDbStorage { for key in &to_delete { batch.delete_cf(&cf, key); } - self.db.write(batch) + self.db + .write(batch) .map_err(|e| AegisError::StorageQuery(e.to_string()))?; Ok(to_delete.len()) } - fn delete_soft_deleted_tuples_before(&self, _partition_id: &PartitionId, _cutoff: DateTime) -> AegisResult { + fn delete_soft_deleted_tuples_before( + &self, + _partition_id: &PartitionId, + _cutoff: DateTime, + ) -> AegisResult { // RocksDB does not maintain soft-deleted tuples — tuples are removed // from the CF on delete. Nothing to clean up. Ok(0) } - fn recover_from_events(&self, partition_id: &PartitionId, to_revision: Option) -> AegisResult { - let _guard = self.revision_mutex.lock() + fn recover_from_events( + &self, + partition_id: &PartitionId, + to_revision: Option, + ) -> AegisResult { + let _guard = self + .revision_mutex + .lock() .map_err(|_| AegisError::Internal("revision mutex poisoned".into()))?; - let cf_tuples = self.db.cf_handle(CF_TUPLES) + let cf_tuples = self + .db + .cf_handle(CF_TUPLES) .ok_or_else(|| AegisError::StorageConnection("missing tuples cf".into()))?; - let cf_idx = self.db.cf_handle(CF_IDX_OBJECT) + let cf_idx = self + .db + .cf_handle(CF_IDX_OBJECT) .ok_or_else(|| AegisError::StorageConnection("missing idx_object cf".into()))?; - let cf_events = self.db.cf_handle(CF_EVENTS) + let cf_events = self + .db + .cf_handle(CF_EVENTS) .ok_or_else(|| AegisError::StorageConnection("missing events cf".into()))?; - let cf_meta = self.db.cf_handle(CF_META) + let cf_meta = self + .db + .cf_handle(CF_META) .ok_or_else(|| AegisError::StorageConnection("missing meta cf".into()))?; let pid_prefix = format!("{}:", partition_id.as_str()); @@ -1126,7 +1454,8 @@ impl StorageBackend for RocksDbStorage { } delete_batch.delete_cf(&cf_idx, &key); } - self.db.write(delete_batch) + self.db + .write(delete_batch) .map_err(|e| AegisError::StorageQuery(e.to_string()))?; let events_iter = self.db.prefix_iterator_cf(&cf_events, pid_prefix_bytes); @@ -1145,6 +1474,7 @@ impl StorageBackend for RocksDbStorage { let relation = event["relation"].as_str().unwrap_or(""); let object = event["object"].as_str().unwrap_or(""); let revision = Revision::new(rev); + #[allow(clippy::collapsible_if)] if let Some(target) = to_revision { if revision > target { continue; @@ -1164,13 +1494,15 @@ impl StorageBackend for RocksDbStorage { let val = serde_json::to_string(&tuple) .map_err(|e| AegisError::StorageQuery(e.to_string()))?; let pk = tuple_key(partition_id.as_str(), subject, relation, object); - let idx_key = object_idx_key(partition_id.as_str(), object, relation, subject); + let idx_key = + object_idx_key(partition_id.as_str(), object, relation, subject); replay_batch.put_cf(&cf_tuples, &pk, val.as_bytes()); - replay_batch.put_cf(&cf_idx, &idx_key, &[]); + replay_batch.put_cf(&cf_idx, &idx_key, []); } "remove" => { let pk = tuple_key(partition_id.as_str(), subject, relation, object); - let idx_key = object_idx_key(partition_id.as_str(), object, relation, subject); + let idx_key = + object_idx_key(partition_id.as_str(), object, relation, subject); replay_batch.delete_cf(&cf_tuples, &pk); replay_batch.delete_cf(&cf_idx, &idx_key); } @@ -1181,11 +1513,17 @@ impl StorageBackend for RocksDbStorage { } } - self.db.write(replay_batch) + self.db + .write(replay_batch) .map_err(|e| AegisError::StorageQuery(e.to_string()))?; if last_revision != Revision::ZERO { - self.db.put_cf(&cf_meta, META_REVISION.as_bytes(), &last_revision.as_u64().to_le_bytes()) + self.db + .put_cf( + &cf_meta, + META_REVISION.as_bytes(), + last_revision.as_u64().to_le_bytes(), + ) .map_err(|e| AegisError::StorageQuery(e.to_string()))?; } @@ -1193,7 +1531,9 @@ impl StorageBackend for RocksDbStorage { } fn compact_events(&self, partition_id: &PartitionId) -> AegisResult { - let cf = self.db.cf_handle(CF_EVENTS) + let cf = self + .db + .cf_handle(CF_EVENTS) .ok_or_else(|| AegisError::StorageConnection("missing events cf".into()))?; let pid_prefix = format!("{}:", partition_id.as_str()).into_bytes(); let iter = self.db.prefix_iterator_cf(&cf, &pid_prefix); @@ -1231,7 +1571,8 @@ impl StorageBackend for RocksDbStorage { for key in &to_delete { batch.delete_cf(&cf, key); } - self.db.write(batch) + self.db + .write(batch) .map_err(|e| AegisError::StorageQuery(e.to_string()))?; Ok(to_delete.len()) } @@ -1242,7 +1583,9 @@ impl StorageBackend for RocksDbStorage { } fn verify_audit_chain(&self, partition_id: &PartitionId) -> AegisResult> { - let cf = self.db.cf_handle(CF_EVENTS) + let cf = self + .db + .cf_handle(CF_EVENTS) .ok_or_else(|| AegisError::StorageConnection("missing events cf".into()))?; let pid_prefix = format!("{}:", partition_id.as_str()).into_bytes(); let iter = self.db.prefix_iterator_cf(&cf, &pid_prefix); @@ -1259,10 +1602,7 @@ impl StorageBackend for RocksDbStorage { let subject = event["subject"].as_str().unwrap_or(""); let relation = event["relation"].as_str().unwrap_or(""); let object = event["object"].as_str().unwrap_or(""); - let metadata = event["metadata"].as_str().or_else(|| { - // metadata can be JSON null or absent - None - }); + let metadata = event["metadata"].as_str().or(None); let timestamp = event["timestamp"].as_str().unwrap_or(""); let identity = event["identity"].as_str(); let prev_hash = event["previous_hash"].as_str().unwrap_or(""); @@ -1309,19 +1649,29 @@ impl StorageBackend for RocksDbStorage { events: &[AuditEntry], revision: Revision, ) -> AegisResult<()> { - let cf_meta = self.db.cf_handle(CF_META) + let cf_meta = self + .db + .cf_handle(CF_META) .ok_or_else(|| AegisError::StorageConnection("missing meta cf".into()))?; - let cf_tuples = self.db.cf_handle(CF_TUPLES) + let cf_tuples = self + .db + .cf_handle(CF_TUPLES) .ok_or_else(|| AegisError::StorageConnection("missing tuples cf".into()))?; - let cf_idx = self.db.cf_handle(CF_IDX_OBJECT) + let cf_idx = self + .db + .cf_handle(CF_IDX_OBJECT) .ok_or_else(|| AegisError::StorageConnection("missing idx_object cf".into()))?; - let cf_events = self.db.cf_handle(CF_EVENTS) + let cf_events = self + .db + .cf_handle(CF_EVENTS) .ok_or_else(|| AegisError::StorageConnection("missing events cf".into()))?; let mut batch = rocksdb::WriteBatch::default(); // Clear existing data (iterate and delete) - let iter = self.db.iterator_cf(&cf_tuples, rocksdb::IteratorMode::Start); + let iter = self + .db + .iterator_cf(&cf_tuples, rocksdb::IteratorMode::Start); for item in iter { let (key, _) = item.map_err(|e| AegisError::StorageQuery(e.to_string()))?; batch.delete_cf(&cf_tuples, &key); @@ -1331,19 +1681,31 @@ impl StorageBackend for RocksDbStorage { let (key, _) = item.map_err(|e| AegisError::StorageQuery(e.to_string()))?; batch.delete_cf(&cf_idx, &key); } - let iter = self.db.iterator_cf(&cf_events, rocksdb::IteratorMode::Start); + let iter = self + .db + .iterator_cf(&cf_events, rocksdb::IteratorMode::Start); for item in iter { let (key, _) = item.map_err(|e| AegisError::StorageQuery(e.to_string()))?; batch.delete_cf(&cf_events, &key); } for tuple in tuples { - let pk = tuple_key(partition_id.as_str(), tuple.subject.as_str(), tuple.relation.as_str(), tuple.object.as_str()); - let idx_key = object_idx_key(partition_id.as_str(), tuple.object.as_str(), tuple.relation.as_str(), tuple.subject.as_str()); + let pk = tuple_key( + partition_id.as_str(), + tuple.subject.as_str(), + tuple.relation.as_str(), + tuple.object.as_str(), + ); + let idx_key = object_idx_key( + partition_id.as_str(), + tuple.object.as_str(), + tuple.relation.as_str(), + tuple.subject.as_str(), + ); let val = serde_json::to_string(tuple) .map_err(|e| AegisError::StorageQuery(e.to_string()))?; batch.put_cf(&cf_tuples, &pk, val.as_bytes()); - batch.put_cf(&cf_idx, &idx_key, &[]); + batch.put_cf(&cf_idx, &idx_key, []); } for event in events { @@ -1371,9 +1733,14 @@ impl StorageBackend for RocksDbStorage { } // Set revision - batch.put_cf(&cf_meta, META_REVISION.as_bytes(), &revision.as_u64().to_le_bytes()); + batch.put_cf( + &cf_meta, + META_REVISION.as_bytes(), + revision.as_u64().to_le_bytes(), + ); - self.db.write(batch) + self.db + .write(batch) .map_err(|e| AegisError::StorageQuery(e.to_string()))?; Ok(()) } @@ -1391,7 +1758,8 @@ impl StorageBackend for RocksDbStorage { while iter.valid() { if let (Some(key), Some(value)) = (iter.key(), iter.value()) { let key_str = String::from_utf8_lossy(key); - if let Ok(version_num) = key_str.parse::() { + #[allow(clippy::collapsible_if)] + if key_str.parse::().is_ok() { if let Ok(pv) = serde_json::from_slice::(value) { versions.push(pv); } @@ -1410,8 +1778,8 @@ impl StorageBackend for RocksDbStorage { .ok_or_else(|| AegisError::StorageConnection("missing policy_versions cf".into()))?; let key = version.version.to_string(); - let value = serde_json::to_string(version) - .map_err(|e| AegisError::Internal(e.to_string()))?; + let value = + serde_json::to_string(version).map_err(|e| AegisError::Internal(e.to_string()))?; self.db .put_cf(&versions_cf, key.as_bytes(), value.as_bytes()) @@ -1444,8 +1812,8 @@ impl StorageBackend for RocksDbStorage { .ok_or_else(|| AegisError::StorageConnection("missing policy_drafts cf".into()))?; let key = draft.id.to_string(); - let value = serde_json::to_string(draft) - .map_err(|e| AegisError::Internal(e.to_string()))?; + let value = + serde_json::to_string(draft).map_err(|e| AegisError::Internal(e.to_string()))?; self.db .put_cf(&cf, key.as_bytes(), value.as_bytes()) @@ -1498,8 +1866,8 @@ impl StorageBackend for RocksDbStorage { .ok_or_else(|| AegisError::StorageConnection("missing analysis_schedules cf".into()))?; let key = schedule.id.to_string(); - let value = serde_json::to_string(schedule) - .map_err(|e| AegisError::Internal(e.to_string()))?; + let value = + serde_json::to_string(schedule).map_err(|e| AegisError::Internal(e.to_string()))?; self.db .put_cf(&cf, key.as_bytes(), value.as_bytes()) @@ -1535,8 +1903,7 @@ impl StorageBackend for RocksDbStorage { .ok_or_else(|| AegisError::StorageConnection("missing analysis_runs cf".into()))?; let key = run.id.to_string(); - let value = serde_json::to_string(run) - .map_err(|e| AegisError::Internal(e.to_string()))?; + let value = serde_json::to_string(run).map_err(|e| AegisError::Internal(e.to_string()))?; self.db .put_cf(&cf, key.as_bytes(), value.as_bytes()) @@ -1551,8 +1918,8 @@ impl StorageBackend for RocksDbStorage { .ok_or_else(|| AegisError::StorageConnection("missing enforcement_events cf".into()))?; let key = event.id.to_string(); - let value = serde_json::to_string(event) - .map_err(|e| AegisError::Internal(e.to_string()))?; + let value = + serde_json::to_string(event).map_err(|e| AegisError::Internal(e.to_string()))?; self.db .put_cf(&cf, key.as_bytes(), value.as_bytes()) @@ -1563,13 +1930,14 @@ impl StorageBackend for RocksDbStorage { /// A RocksDB transaction using WriteBatch for atomicity. pub struct RocksDbTransaction { - db: DB, + db: Arc, partition_id: String, batch: rocksdb::WriteBatch, - cf_tuples: rocksdb::ColumnFamily, - cf_idx: rocksdb::ColumnFamily, - cf_events: rocksdb::ColumnFamily, - cf_meta: rocksdb::ColumnFamily, + cf_tuples: &'static rocksdb::ColumnFamily, + cf_idx: &'static rocksdb::ColumnFamily, + cf_events: &'static rocksdb::ColumnFamily, + cf_meta: &'static rocksdb::ColumnFamily, + #[allow(dead_code)] node_id: Uuid, revision_mutex: std::sync::Arc>, actor_identity: Option, @@ -1578,7 +1946,7 @@ pub struct RocksDbTransaction { } impl RocksDbTransaction { - fn write_pending_events(&self, revision: Revision) -> AegisResult<()> { + fn write_pending_events(&mut self, revision: Revision) -> AegisResult<()> { let now = Utc::now().to_rfc3339(); let mut previous_hash = self.get_last_event_hash()?; for (action, subject, relation, object, metadata) in &self.pending_events { @@ -1616,8 +1984,9 @@ impl RocksDbTransaction { Ok(()) } + #[allow(dead_code)] fn put_tuple_to_batch( - &self, + &mut self, partition_id: &str, subject: &str, relation: &str, @@ -1627,11 +1996,17 @@ impl RocksDbTransaction { let pk = tuple_key(partition_id, subject, relation, object); let idx_key = object_idx_key(partition_id, object, relation, subject); self.batch.put_cf(&self.cf_tuples, &pk, value); - self.batch.put_cf(&self.cf_idx, &idx_key, &[]); + self.batch.put_cf(&self.cf_idx, &idx_key, []); Ok(()) } - fn delete_tuple_from_batch(&self, partition_id: &str, subject: &str, relation: &str, object: &str) -> AegisResult<()> { + fn delete_tuple_from_batch( + &mut self, + partition_id: &str, + subject: &str, + relation: &str, + object: &str, + ) -> AegisResult<()> { let pk = tuple_key(partition_id, subject, relation, object); let idx_key = object_idx_key(partition_id, object, relation, subject); self.batch.delete_cf(&self.cf_tuples, &pk); @@ -1648,6 +2023,7 @@ impl RocksDbTransaction { if !key.starts_with(&pid_prefix) { break; } + #[allow(clippy::collapsible_if)] if let Ok(event) = serde_json::from_slice::(&value) { if let Some(h) = event["event_hash"].as_str() { last_hash = h.to_string(); @@ -1666,20 +2042,32 @@ impl StorageTransaction for RocksDbTransaction { } fn write(&mut self, partition_id: &PartitionId, tuple: &RelationshipTuple) -> AegisResult<()> { - let val = serde_json::to_string(tuple) - .map_err(|e| AegisError::StorageQuery(e.to_string()))?; + let val = + serde_json::to_string(tuple).map_err(|e| AegisError::StorageQuery(e.to_string()))?; // Remove existing if present - let pk = tuple_key(partition_id.as_str(), tuple.subject.as_str(), tuple.relation.as_str(), tuple.object.as_str()); - let idx_key = object_idx_key(partition_id.as_str(), tuple.object.as_str(), tuple.relation.as_str(), tuple.subject.as_str()); + let pk = tuple_key( + partition_id.as_str(), + tuple.subject.as_str(), + tuple.relation.as_str(), + tuple.object.as_str(), + ); + let idx_key = object_idx_key( + partition_id.as_str(), + tuple.object.as_str(), + tuple.relation.as_str(), + tuple.subject.as_str(), + ); self.batch.delete_cf(&self.cf_tuples, &pk); self.batch.delete_cf(&self.cf_idx, &idx_key); // Insert new self.batch.put_cf(&self.cf_tuples, &pk, val.as_bytes()); - self.batch.put_cf(&self.cf_idx, &idx_key, &[]); + self.batch.put_cf(&self.cf_idx, &idx_key, []); - let metadata_json = tuple.metadata.as_ref() - .map(|m| serde_json::to_string(m)) + let metadata_json = tuple + .metadata + .as_ref() + .map(serde_json::to_string) .transpose() .map_err(|e| AegisError::MetadataValidation(e.to_string()))?; self.pending_events.push(( @@ -1694,7 +2082,12 @@ impl StorageTransaction for RocksDbTransaction { } fn delete(&mut self, partition_id: &PartitionId, key: &TupleKey) -> AegisResult<()> { - self.delete_tuple_from_batch(partition_id.as_str(), key.subject.as_str(), key.relation.as_str(), key.object.as_str())?; + self.delete_tuple_from_batch( + partition_id.as_str(), + key.subject.as_str(), + key.relation.as_str(), + key.object.as_str(), + )?; self.pending_events.push(( "remove".to_string(), key.subject.as_str().to_string(), @@ -1708,41 +2101,54 @@ impl StorageTransaction for RocksDbTransaction { fn savepoint(&self, _name: &str) -> AegisResult<()> { Err(AegisError::OperationNotPermitted( - "savepoints not supported on RocksDB backend; use SQLite for transactional semantics".into(), + "savepoints not supported on RocksDB backend; use SQLite for transactional semantics" + .into(), )) } fn rollback_to_savepoint(&self, _name: &str) -> AegisResult<()> { Err(AegisError::OperationNotPermitted( - "savepoints not supported on RocksDB backend; use SQLite for transactional semantics".into(), + "savepoints not supported on RocksDB backend; use SQLite for transactional semantics" + .into(), )) } fn release_savepoint(&self, _name: &str) -> AegisResult<()> { Err(AegisError::OperationNotPermitted( - "savepoints not supported on RocksDB backend; use SQLite for transactional semantics".into(), + "savepoints not supported on RocksDB backend; use SQLite for transactional semantics" + .into(), )) } fn commit(self: Box) -> AegisResult { - let s = *self; - let _guard = s.revision_mutex.lock() - .map_err(|_| AegisError::Internal("revision mutex poisoned".into()))?; - - // Read current revision atomically - let rev = s.db.get_cf(&s.cf_meta, META_REVISION.as_bytes()) - .map_err(|e| AegisError::StorageQuery(e.to_string()))? - .map(|v| u64::from_le_bytes(v.as_slice().try_into().unwrap_or([0; 8]))) - .unwrap_or(0); - let new_rev = rev.checked_add(1) - .ok_or_else(|| AegisError::Internal("revision overflow".into()))?; - let revision = Revision::new(new_rev); + let mut s = *self; + let revision = { + let _guard = s + .revision_mutex + .lock() + .map_err(|_| AegisError::Internal("revision mutex poisoned".into()))?; + + // Read current revision atomically + let rev = + s.db.get_cf(&s.cf_meta, META_REVISION.as_bytes()) + .map_err(|e| AegisError::StorageQuery(e.to_string()))? + .map(|v| u64::from_le_bytes(v.as_slice().try_into().unwrap_or([0; 8]))) + .unwrap_or(0); + let new_rev = rev + .checked_add(1) + .ok_or_else(|| AegisError::Internal("revision overflow".into()))?; + Revision::new(new_rev) + }; // _guard dropped here // Write pending events with final revision s.write_pending_events(revision)?; // Update revision in batch - s.batch.put_cf(&s.cf_meta, META_REVISION.as_bytes(), &new_rev.to_le_bytes()); + s.batch.put_cf( + &s.cf_meta, + META_REVISION.as_bytes(), + revision.as_u64().to_le_bytes(), + ); // Write the batch atomically s.db.write(s.batch) @@ -1764,8 +2170,7 @@ mod tests { use std::fs; fn temp_dir() -> String { - let dir = std::env::temp_dir() - .join(format!("aegis_rocksdb_test_{}", Uuid::new_v4())); + let dir = std::env::temp_dir().join(format!("aegis_rocksdb_test_{}", Uuid::new_v4())); let path = dir.to_str().unwrap().to_string(); let _ = fs::remove_dir_all(&path); path @@ -1792,10 +2197,14 @@ mod tests { Relation::new("owner").unwrap(), ResourceId::new("repo:fluxbus").unwrap(), ); - let rev = storage.write_tuple(&PartitionId::default(), &tuple).unwrap(); + let rev = storage + .write_tuple(&PartitionId::default(), &tuple) + .unwrap(); assert!(rev.as_u64() > 0); - let found = storage.read_tuple(&PartitionId::default(), &tuple.key()).unwrap(); + let found = storage + .read_tuple(&PartitionId::default(), &tuple.key()) + .unwrap(); assert!(found.is_some()); assert_eq!(found.unwrap().subject.as_str(), "user:alice"); } @@ -1815,7 +2224,9 @@ mod tests { Relation::new("viewer").unwrap(), ResourceId::new("repo:other").unwrap(), ); - storage.write_tuple(&PartitionId::default(), &tuple).unwrap(); + storage + .write_tuple(&PartitionId::default(), &tuple) + .unwrap(); assert!(storage.has_tuple(&PartitionId::default(), &key).unwrap()); } @@ -1827,12 +2238,24 @@ mod tests { Relation::new("owner").unwrap(), ResourceId::new("repo:fluxbus").unwrap(), ); - storage.write_tuple(&PartitionId::default(), &tuple).unwrap(); - assert!(storage.has_tuple(&PartitionId::default(), &tuple.key()).unwrap()); + storage + .write_tuple(&PartitionId::default(), &tuple) + .unwrap(); + assert!( + storage + .has_tuple(&PartitionId::default(), &tuple.key()) + .unwrap() + ); - let del_rev = storage.delete_tuple(&PartitionId::default(), &tuple.key()).unwrap(); + let del_rev = storage + .delete_tuple(&PartitionId::default(), &tuple.key()) + .unwrap(); assert!(del_rev.as_u64() > 0); - assert!(!storage.has_tuple(&PartitionId::default(), &tuple.key()).unwrap()); + assert!( + !storage + .has_tuple(&PartitionId::default(), &tuple.key()) + .unwrap() + ); } #[test] @@ -1848,12 +2271,28 @@ mod tests { Relation::new("viewer").unwrap(), ResourceId::new("repo:2").unwrap(), ); - let rev = storage.write_tuples_batch(&PartitionId::default(), &[t1, t2]).unwrap(); + let rev = storage + .write_tuples_batch(&PartitionId::default(), &[t1, t2]) + .unwrap(); assert!(rev.as_u64() > 0); - let all = storage.list_by_subject(&PartitionId::default(), &SubjectId::new("user:a").unwrap(), None, &ConsistencyMode::MinimizeLatency).unwrap(); + let all = storage + .list_by_subject( + &PartitionId::default(), + &SubjectId::new("user:a").unwrap(), + None, + &ConsistencyMode::MinimizeLatency, + ) + .unwrap(); assert_eq!(all.len(), 1); - let all = storage.list_by_subject(&PartitionId::default(), &SubjectId::new("user:b").unwrap(), None, &ConsistencyMode::MinimizeLatency).unwrap(); + let all = storage + .list_by_subject( + &PartitionId::default(), + &SubjectId::new("user:b").unwrap(), + None, + &ConsistencyMode::MinimizeLatency, + ) + .unwrap(); assert_eq!(all.len(), 1); } @@ -1864,17 +2303,65 @@ mod tests { let bob = SubjectId::new("user:bob").unwrap(); let repo = ResourceId::new("repo:fluxbus").unwrap(); - storage.write_tuple(&PartitionId::default(), &RelationshipTuple::new(alice.clone(), Relation::new("owner").unwrap(), repo.clone())).unwrap(); - storage.write_tuple(&PartitionId::default(), &RelationshipTuple::new(alice.clone(), Relation::new("viewer").unwrap(), repo.clone())).unwrap(); - storage.write_tuple(&PartitionId::default(), &RelationshipTuple::new(bob.clone(), Relation::new("viewer").unwrap(), repo.clone())).unwrap(); - - let alice_tuples = storage.list_by_subject(&PartitionId::default(), &alice, None, &ConsistencyMode::MinimizeLatency).unwrap(); + storage + .write_tuple( + &PartitionId::default(), + &RelationshipTuple::new( + alice.clone(), + Relation::new("owner").unwrap(), + repo.clone(), + ), + ) + .unwrap(); + storage + .write_tuple( + &PartitionId::default(), + &RelationshipTuple::new( + alice.clone(), + Relation::new("viewer").unwrap(), + repo.clone(), + ), + ) + .unwrap(); + storage + .write_tuple( + &PartitionId::default(), + &RelationshipTuple::new( + bob.clone(), + Relation::new("viewer").unwrap(), + repo.clone(), + ), + ) + .unwrap(); + + let alice_tuples = storage + .list_by_subject( + &PartitionId::default(), + &alice, + None, + &ConsistencyMode::MinimizeLatency, + ) + .unwrap(); assert_eq!(alice_tuples.len(), 2); - let alice_owner = storage.list_by_subject(&PartitionId::default(), &alice, Some(&Relation::new("owner").unwrap()), &ConsistencyMode::MinimizeLatency).unwrap(); + let alice_owner = storage + .list_by_subject( + &PartitionId::default(), + &alice, + Some(&Relation::new("owner").unwrap()), + &ConsistencyMode::MinimizeLatency, + ) + .unwrap(); assert_eq!(alice_owner.len(), 1); - let bob_tuples = storage.list_by_subject(&PartitionId::default(), &bob, None, &ConsistencyMode::MinimizeLatency).unwrap(); + let bob_tuples = storage + .list_by_subject( + &PartitionId::default(), + &bob, + None, + &ConsistencyMode::MinimizeLatency, + ) + .unwrap(); assert_eq!(bob_tuples.len(), 1); } @@ -1884,23 +2371,65 @@ mod tests { let repo_a = ResourceId::new("repo:a").unwrap(); let repo_b = ResourceId::new("repo:b").unwrap(); - storage.write_tuple(&PartitionId::default(), &RelationshipTuple::new( - SubjectId::new("user:alice").unwrap(), Relation::new("owner").unwrap(), repo_a.clone(), - )).unwrap(); - storage.write_tuple(&PartitionId::default(), &RelationshipTuple::new( - SubjectId::new("user:bob").unwrap(), Relation::new("viewer").unwrap(), repo_a.clone(), - )).unwrap(); - storage.write_tuple(&PartitionId::default(), &RelationshipTuple::new( - SubjectId::new("user:alice").unwrap(), Relation::new("owner").unwrap(), repo_b.clone(), - )).unwrap(); - - let a_tuples = storage.list_by_object(&PartitionId::default(), &repo_a, None, &ConsistencyMode::MinimizeLatency).unwrap(); + storage + .write_tuple( + &PartitionId::default(), + &RelationshipTuple::new( + SubjectId::new("user:alice").unwrap(), + Relation::new("owner").unwrap(), + repo_a.clone(), + ), + ) + .unwrap(); + storage + .write_tuple( + &PartitionId::default(), + &RelationshipTuple::new( + SubjectId::new("user:bob").unwrap(), + Relation::new("viewer").unwrap(), + repo_a.clone(), + ), + ) + .unwrap(); + storage + .write_tuple( + &PartitionId::default(), + &RelationshipTuple::new( + SubjectId::new("user:alice").unwrap(), + Relation::new("owner").unwrap(), + repo_b.clone(), + ), + ) + .unwrap(); + + let a_tuples = storage + .list_by_object( + &PartitionId::default(), + &repo_a, + None, + &ConsistencyMode::MinimizeLatency, + ) + .unwrap(); assert_eq!(a_tuples.len(), 2); - let a_owners = storage.list_by_object(&PartitionId::default(), &repo_a, Some(&Relation::new("owner").unwrap()), &ConsistencyMode::MinimizeLatency).unwrap(); + let a_owners = storage + .list_by_object( + &PartitionId::default(), + &repo_a, + Some(&Relation::new("owner").unwrap()), + &ConsistencyMode::MinimizeLatency, + ) + .unwrap(); assert_eq!(a_owners.len(), 1); - let b_tuples = storage.list_by_object(&PartitionId::default(), &repo_b, None, &ConsistencyMode::MinimizeLatency).unwrap(); + let b_tuples = storage + .list_by_object( + &PartitionId::default(), + &repo_b, + None, + &ConsistencyMode::MinimizeLatency, + ) + .unwrap(); assert_eq!(b_tuples.len(), 1); } @@ -1910,17 +2439,40 @@ mod tests { let alice = SubjectId::new("user:alice").unwrap(); let repo = ResourceId::new("repo:fluxbus").unwrap(); - storage.write_tuple(&PartitionId::default(), &RelationshipTuple::new( - alice.clone(), Relation::new("owner").unwrap(), repo.clone(), - )).unwrap(); - storage.write_tuple(&PartitionId::default(), &RelationshipTuple::new( - alice.clone(), Relation::new("viewer").unwrap(), repo.clone(), - )).unwrap(); - - let rev = storage.delete_subject(&PartitionId::default(), &alice).unwrap(); + storage + .write_tuple( + &PartitionId::default(), + &RelationshipTuple::new( + alice.clone(), + Relation::new("owner").unwrap(), + repo.clone(), + ), + ) + .unwrap(); + storage + .write_tuple( + &PartitionId::default(), + &RelationshipTuple::new( + alice.clone(), + Relation::new("viewer").unwrap(), + repo.clone(), + ), + ) + .unwrap(); + + let rev = storage + .delete_subject(&PartitionId::default(), &alice) + .unwrap(); assert!(rev.as_u64() > 0); - let tuples = storage.list_by_subject(&PartitionId::default(), &alice, None, &ConsistencyMode::MinimizeLatency).unwrap(); + let tuples = storage + .list_by_subject( + &PartitionId::default(), + &alice, + None, + &ConsistencyMode::MinimizeLatency, + ) + .unwrap(); assert_eq!(tuples.len(), 0); } @@ -1929,31 +2481,72 @@ mod tests { let storage = make_storage(); let repo = ResourceId::new("repo:fluxbus").unwrap(); - storage.write_tuple(&PartitionId::default(), &RelationshipTuple::new( - SubjectId::new("user:alice").unwrap(), Relation::new("owner").unwrap(), repo.clone(), - )).unwrap(); - storage.write_tuple(&PartitionId::default(), &RelationshipTuple::new( - SubjectId::new("user:bob").unwrap(), Relation::new("viewer").unwrap(), repo.clone(), - )).unwrap(); - - let rev = storage.delete_object(&PartitionId::default(), &repo).unwrap(); + storage + .write_tuple( + &PartitionId::default(), + &RelationshipTuple::new( + SubjectId::new("user:alice").unwrap(), + Relation::new("owner").unwrap(), + repo.clone(), + ), + ) + .unwrap(); + storage + .write_tuple( + &PartitionId::default(), + &RelationshipTuple::new( + SubjectId::new("user:bob").unwrap(), + Relation::new("viewer").unwrap(), + repo.clone(), + ), + ) + .unwrap(); + + let rev = storage + .delete_object(&PartitionId::default(), &repo) + .unwrap(); assert!(rev.as_u64() > 0); - let tuples = storage.list_by_object(&PartitionId::default(), &repo, None, &ConsistencyMode::MinimizeLatency).unwrap(); + let tuples = storage + .list_by_object( + &PartitionId::default(), + &repo, + None, + &ConsistencyMode::MinimizeLatency, + ) + .unwrap(); assert_eq!(tuples.len(), 0); } #[test] fn test_current_revision() { let storage = make_storage(); - assert_eq!(storage.current_revision(&PartitionId::default()).unwrap().as_u64(), 0); - - storage.write_tuple(&PartitionId::default(), &RelationshipTuple::new( - SubjectId::new("user:alice").unwrap(), Relation::new("owner").unwrap(), - ResourceId::new("repo:fluxbus").unwrap(), - )).unwrap(); + assert_eq!( + storage + .current_revision(&PartitionId::default()) + .unwrap() + .as_u64(), + 0 + ); - assert_eq!(storage.current_revision(&PartitionId::default()).unwrap().as_u64(), 1); + storage + .write_tuple( + &PartitionId::default(), + &RelationshipTuple::new( + SubjectId::new("user:alice").unwrap(), + Relation::new("owner").unwrap(), + ResourceId::new("repo:fluxbus").unwrap(), + ), + ) + .unwrap(); + + assert_eq!( + storage + .current_revision(&PartitionId::default()) + .unwrap() + .as_u64(), + 1 + ); } #[test] @@ -1971,11 +2564,23 @@ mod tests { Relation::new("owner").unwrap(), ResourceId::new("repo:fluxbus").unwrap(), ); - let r1 = storage.write_tuple(&PartitionId::default(), &tuple).unwrap(); - let r2 = storage.write_tuple(&PartitionId::default(), &tuple).unwrap(); + let r1 = storage + .write_tuple(&PartitionId::default(), &tuple) + .unwrap(); + let r2 = storage + .write_tuple(&PartitionId::default(), &tuple) + .unwrap(); assert!(r2 > r1); // Only one active tuple after idempotent write - let count = storage.list_by_subject(&PartitionId::default(), &SubjectId::new("user:alice").unwrap(), None, &ConsistencyMode::MinimizeLatency).unwrap().len(); + let count = storage + .list_by_subject( + &PartitionId::default(), + &SubjectId::new("user:alice").unwrap(), + None, + &ConsistencyMode::MinimizeLatency, + ) + .unwrap() + .len(); assert_eq!(count, 1); } @@ -1990,10 +2595,19 @@ mod tests { Relation::new("owner").unwrap(), ResourceId::new("repo:fluxbus").unwrap(), meta, - ).unwrap(); - storage.write_tuple(&PartitionId::default(), &tuple).unwrap(); - let found = storage.read_tuple(&PartitionId::default(), &tuple.key()).unwrap().unwrap(); - assert_eq!(found.metadata.as_ref().unwrap().get("key1").unwrap(), "val1"); + ) + .unwrap(); + storage + .write_tuple(&PartitionId::default(), &tuple) + .unwrap(); + let found = storage + .read_tuple(&PartitionId::default(), &tuple.key()) + .unwrap() + .unwrap(); + assert_eq!( + found.metadata.as_ref().unwrap().get("key1").unwrap(), + "val1" + ); } #[test] diff --git a/crates/aegis-core/src/storage/sqlite.rs b/crates/aegis-core/src/storage/sqlite.rs index 6a9593e..2ee84a9 100644 --- a/crates/aegis-core/src/storage/sqlite.rs +++ b/crates/aegis-core/src/storage/sqlite.rs @@ -7,13 +7,14 @@ use crate::storage::traits::{ TupleFilter, }; use crate::types::{ - AuditEntry, ConnectionStats, ConsistencyMode, PaginatedTuples, PaginationCursor, PaginationParams, PartitionId, Relation, - RelationshipTuple, ResourceId, Revision, RevisionToken, SubjectId, TupleKey, TupleMutation, + AuditEntry, ConnectionStats, ConsistencyMode, PaginatedTuples, PaginationCursor, + PaginationParams, PartitionId, Relation, RelationshipTuple, ResourceId, Revision, + RevisionToken, SubjectId, TupleKey, TupleMutation, }; use chrono::{DateTime, Utc}; use r2d2::Pool; use r2d2_sqlite::SqliteConnectionManager; -use rusqlite::{params, Connection}; +use rusqlite::{Connection, params}; use serde_json; use std::collections::HashMap; use uuid::Uuid; @@ -40,8 +41,7 @@ const TUPLES_TABLE: &str = " revision_removed INTEGER DEFAULT NULL )"; -const TUPLES_ACTIVE_IDX: &str = - "CREATE UNIQUE INDEX IF NOT EXISTS idx_tuples_active ON _aegis_tuples(subject, relation, object) WHERE revision_removed IS NULL"; +const TUPLES_ACTIVE_IDX: &str = "CREATE UNIQUE INDEX IF NOT EXISTS idx_tuples_active ON _aegis_tuples(subject, relation, object) WHERE revision_removed IS NULL"; const TUPLES_OBJECT_IDX: &str = "CREATE INDEX IF NOT EXISTS idx_tuples_object ON _aegis_tuples(object)"; const TUPLES_SUBJECT_IDX: &str = @@ -180,7 +180,10 @@ struct SqliteConnectionConfigurator { impl r2d2::CustomizeConnection for SqliteConnectionConfigurator { fn on_acquire(&self, conn: &mut Connection) -> Result<(), rusqlite::Error> { - conn.execute_batch(&format!("PRAGMA busy_timeout = {};", self.config.busy_timeout_ms))?; + conn.execute_batch(&format!( + "PRAGMA busy_timeout = {};", + self.config.busy_timeout_ms + ))?; if self.config.wal_mode { conn.execute_batch("PRAGMA journal_mode = WAL;")?; } @@ -214,7 +217,9 @@ impl SqliteStorage { SqliteConnectionManager::file(&config.path) }; - let customizer = SqliteConnectionConfigurator { config: config.clone() }; + let customizer = SqliteConnectionConfigurator { + config: config.clone(), + }; let pool = Pool::builder() .max_size(config.max_readers + 1) // +1 for potential write connection .connection_customizer(Box::new(customizer)) @@ -274,8 +279,12 @@ impl SqliteStorage { let _ = conn.execute_batch("ALTER TABLE _aegis_tuples ADD COLUMN valid_until TEXT"); let _ = conn.execute_batch("ALTER TABLE _aegis_tuples ADD COLUMN condition TEXT"); // V3: Add audit hash columns (no-op if columns already exist) - let _ = conn.execute_batch("ALTER TABLE _aegis_events ADD COLUMN previous_hash TEXT NOT NULL DEFAULT ''"); - let _ = conn.execute_batch("ALTER TABLE _aegis_events ADD COLUMN event_hash TEXT NOT NULL DEFAULT ''"); + let _ = conn.execute_batch( + "ALTER TABLE _aegis_events ADD COLUMN previous_hash TEXT NOT NULL DEFAULT ''", + ); + let _ = conn.execute_batch( + "ALTER TABLE _aegis_events ADD COLUMN event_hash TEXT NOT NULL DEFAULT ''", + ); Ok(()) } @@ -367,6 +376,7 @@ impl SqliteStorage { } /// Append an event to the event log, computing hash-chained integrity fields. + #[allow(clippy::too_many_arguments)] fn append_event( conn: &Connection, revision: Revision, @@ -437,7 +447,7 @@ impl SqliteStorage { .map_err(|e| AegisError::StorageQuery(e.to_string()))?; let rows = stmt - .query_map(params![target_revision], |row| { + .query_map(params![target_revision], |row| { let subject_str: String = row.get(0)?; let relation_str: String = row.get(1)?; let object_str: String = row.get(2)?; @@ -452,7 +462,8 @@ impl SqliteStorage { .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?; let object = ResourceId::new(&object_str) .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?; - let created_at: DateTime = created_at_str.parse().unwrap_or_else(|_| Utc::now()); + let created_at: DateTime = + created_at_str.parse().unwrap_or_else(|_| Utc::now()); let metadata = metadata_json .and_then(|m| serde_json::from_str::>(&m).ok()); let valid_until = valid_until_str.and_then(|s| s.parse::>().ok()); @@ -529,7 +540,11 @@ impl StorageBackend for SqliteStorage { }) } - fn write_tuple(&self, partition_id: &PartitionId, tuple: &RelationshipTuple) -> AegisResult { + fn write_tuple( + &self, + partition_id: &PartitionId, + tuple: &RelationshipTuple, + ) -> AegisResult { self.with_write_tx(|conn| { let revision = Self::bump_revision(conn)?; let metadata_json = tuple @@ -588,7 +603,11 @@ impl StorageBackend for SqliteStorage { }) } - fn write_tuples_batch(&self, partition_id: &PartitionId, tuples: &[RelationshipTuple]) -> AegisResult { + fn write_tuples_batch( + &self, + partition_id: &PartitionId, + tuples: &[RelationshipTuple], + ) -> AegisResult { if tuples.is_empty() { return self.current_revision(partition_id); } @@ -702,7 +721,11 @@ impl StorageBackend for SqliteStorage { }) } - fn delete_subject(&self, partition_id: &PartitionId, subject: &SubjectId) -> AegisResult { + fn delete_subject( + &self, + partition_id: &PartitionId, + subject: &SubjectId, + ) -> AegisResult { self.with_write_tx(|conn| { let revision = Self::bump_revision(conn)?; @@ -726,7 +749,11 @@ impl StorageBackend for SqliteStorage { conn.execute( "UPDATE _aegis_tuples SET revision_removed = ?1 WHERE subject = ?2 AND partition_id = ?3 AND revision_removed IS NULL", - params![revision.as_u64() as i64, subject.as_str(), partition_id.as_str()], + params![ + revision.as_u64() as i64, + subject.as_str(), + partition_id.as_str() + ], ) .map_err(|e| AegisError::StorageQuery(e.to_string()))?; @@ -749,7 +776,11 @@ impl StorageBackend for SqliteStorage { }) } - fn delete_object(&self, partition_id: &PartitionId, object: &ResourceId) -> AegisResult { + fn delete_object( + &self, + partition_id: &PartitionId, + object: &ResourceId, + ) -> AegisResult { self.with_write_tx(|conn| { let revision = Self::bump_revision(conn)?; @@ -773,7 +804,11 @@ impl StorageBackend for SqliteStorage { conn.execute( "UPDATE _aegis_tuples SET revision_removed = ?1 WHERE object = ?2 AND partition_id = ?3 AND revision_removed IS NULL", - params![revision.as_u64() as i64, object.as_str(), partition_id.as_str()], + params![ + revision.as_u64() as i64, + object.as_str(), + partition_id.as_str() + ], ) .map_err(|e| AegisError::StorageQuery(e.to_string()))?; @@ -809,7 +844,11 @@ impl StorageBackend for SqliteStorage { Ok(count > 0) } - fn read_tuple(&self, partition_id: &PartitionId, key: &TupleKey) -> AegisResult> { + fn read_tuple( + &self, + partition_id: &PartitionId, + key: &TupleKey, + ) -> AegisResult> { let conn = self.conn()?; let mut stmt = conn .prepare( @@ -819,40 +858,45 @@ impl StorageBackend for SqliteStorage { ) .map_err(|e| AegisError::StorageQuery(e.to_string()))?; - let result = stmt - .query_row( - params![key.subject.as_str(), key.relation.as_str(), key.object.as_str(), partition_id.as_str()], - |row| { - let subject_str: String = row.get(0)?; - let relation_str: String = row.get(1)?; - let object_str: String = row.get(2)?; - let created_at_str: String = row.get(3)?; - let metadata_json: Option = row.get(4)?; - let valid_until_str: Option = row.get(5)?; - let condition_str: Option = row.get(6)?; + let result = stmt.query_row( + params![ + key.subject.as_str(), + key.relation.as_str(), + key.object.as_str(), + partition_id.as_str() + ], + |row| { + let subject_str: String = row.get(0)?; + let relation_str: String = row.get(1)?; + let object_str: String = row.get(2)?; + let created_at_str: String = row.get(3)?; + let metadata_json: Option = row.get(4)?; + let valid_until_str: Option = row.get(5)?; + let condition_str: Option = row.get(6)?; - let subject = SubjectId::new(&subject_str) - .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?; - let relation = Relation::new(&relation_str) - .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?; - let object = ResourceId::new(&object_str) - .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?; - let created_at: DateTime = created_at_str.parse().unwrap_or_else(|_| Utc::now()); - let metadata = metadata_json - .and_then(|m| serde_json::from_str::>(&m).ok()); - let valid_until = valid_until_str.and_then(|s| s.parse::>().ok()); + let subject = SubjectId::new(&subject_str) + .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?; + let relation = Relation::new(&relation_str) + .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?; + let object = ResourceId::new(&object_str) + .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?; + let created_at: DateTime = + created_at_str.parse().unwrap_or_else(|_| Utc::now()); + let metadata = metadata_json + .and_then(|m| serde_json::from_str::>(&m).ok()); + let valid_until = valid_until_str.and_then(|s| s.parse::>().ok()); - Ok(RelationshipTuple { - subject, - relation, - object, - created_at, - metadata, - valid_until, - condition: condition_str, - }) - }, - ); + Ok(RelationshipTuple { + subject, + relation, + object, + created_at, + metadata, + valid_until, + condition: condition_str, + }) + }, + ); match result { Ok(tuple) => Ok(Some(tuple)), @@ -878,11 +922,15 @@ impl StorageBackend for SqliteStorage { let revision_filter = match consistency { ConsistencyMode::AtRevision(rev) => { let r = rev.as_u64() as i64; - format!("revision_added <= {r} AND (revision_removed IS NULL OR revision_removed > {r})") + format!( + "revision_added <= {r} AND (revision_removed IS NULL OR revision_removed > {r})" + ) } _ => "revision_removed IS NULL".to_string(), }; - let (sql, params_vec): (String, Vec>) = if let Some(rel) = relation { + let (sql, params_vec): (String, Vec>) = if let Some(rel) = + relation + { ( format!( "SELECT subject, relation, object, created_at, metadata, valid_until, condition FROM _aegis_tuples @@ -911,7 +959,8 @@ impl StorageBackend for SqliteStorage { .prepare(&sql) .map_err(|e| AegisError::StorageQuery(e.to_string()))?; - let params_refs: Vec<&dyn rusqlite::types::ToSql> = params_vec.iter().map(|p| p.as_ref()).collect(); + let params_refs: Vec<&dyn rusqlite::types::ToSql> = + params_vec.iter().map(|p| p.as_ref()).collect(); let rows = stmt .query_map(params_refs.as_slice(), |row| { let subject_str: String = row.get(0)?; @@ -928,7 +977,8 @@ impl StorageBackend for SqliteStorage { .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?; let object = ResourceId::new(&object_str) .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?; - let created_at: DateTime = created_at_str.parse().unwrap_or_else(|_| Utc::now()); + let created_at: DateTime = + created_at_str.parse().unwrap_or_else(|_| Utc::now()); let metadata = metadata_json .and_then(|m| serde_json::from_str::>(&m).ok()); let valid_until = valid_until_str.and_then(|s| s.parse::>().ok()); @@ -947,9 +997,7 @@ impl StorageBackend for SqliteStorage { let mut results = Vec::new(); for row in rows { - results.push( - row.map_err(|e| AegisError::StorageQuery(e.to_string()))?, - ); + results.push(row.map_err(|e| AegisError::StorageQuery(e.to_string()))?); } Ok(results) } @@ -971,11 +1019,15 @@ impl StorageBackend for SqliteStorage { let revision_filter = match consistency { ConsistencyMode::AtRevision(rev) => { let r = rev.as_u64() as i64; - format!("revision_added <= {r} AND (revision_removed IS NULL OR revision_removed > {r})") + format!( + "revision_added <= {r} AND (revision_removed IS NULL OR revision_removed > {r})" + ) } _ => "revision_removed IS NULL".to_string(), }; - let (sql, params_vec): (String, Vec>) = if let Some(rel) = relation { + let (sql, params_vec): (String, Vec>) = if let Some(rel) = + relation + { ( format!( "SELECT subject, relation, object, created_at, metadata, valid_until, condition FROM _aegis_tuples @@ -1004,7 +1056,8 @@ impl StorageBackend for SqliteStorage { .prepare(&sql) .map_err(|e| AegisError::StorageQuery(e.to_string()))?; - let params_refs: Vec<&dyn rusqlite::types::ToSql> = params_vec.iter().map(|p| p.as_ref()).collect(); + let params_refs: Vec<&dyn rusqlite::types::ToSql> = + params_vec.iter().map(|p| p.as_ref()).collect(); let rows = stmt .query_map(params_refs.as_slice(), |row| { let subject_str: String = row.get(0)?; @@ -1021,7 +1074,8 @@ impl StorageBackend for SqliteStorage { .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?; let object = ResourceId::new(&object_str) .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?; - let created_at: DateTime = created_at_str.parse().unwrap_or_else(|_| Utc::now()); + let created_at: DateTime = + created_at_str.parse().unwrap_or_else(|_| Utc::now()); let metadata = metadata_json .and_then(|m| serde_json::from_str::>(&m).ok()); let valid_until = valid_until_str.and_then(|s| s.parse::>().ok()); @@ -1040,9 +1094,7 @@ impl StorageBackend for SqliteStorage { let mut results = Vec::new(); for row in rows { - results.push( - row.map_err(|e| AegisError::StorageQuery(e.to_string()))?, - ); + results.push(row.map_err(|e| AegisError::StorageQuery(e.to_string()))?); } Ok(results) } @@ -1079,7 +1131,8 @@ impl StorageBackend for SqliteStorage { .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?; let object = ResourceId::new(&object_str) .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?; - let created_at: DateTime = created_at_str.parse().unwrap_or_else(|_| Utc::now()); + let created_at: DateTime = + created_at_str.parse().unwrap_or_else(|_| Utc::now()); let metadata = metadata_json .and_then(|m| serde_json::from_str::>(&m).ok()); let valid_until = valid_until_str.and_then(|s| s.parse::>().ok()); @@ -1099,9 +1152,7 @@ impl StorageBackend for SqliteStorage { let mut results = Vec::new(); for row in rows { - results.push( - row.map_err(|e| AegisError::StorageQuery(e.to_string()))?, - ); + results.push(row.map_err(|e| AegisError::StorageQuery(e.to_string()))?); } Ok(results) } @@ -1124,12 +1175,15 @@ impl StorageBackend for SqliteStorage { let revision_filter = match consistency { ConsistencyMode::AtRevision(rev) => { let r = rev.as_u64() as i64; - format!("revision_added <= {r} AND (revision_removed IS NULL OR revision_removed > {r})") + format!( + "revision_added <= {r} AND (revision_removed IS NULL OR revision_removed > {r})" + ) } _ => "revision_removed IS NULL".to_string(), }; let mut conditions = vec!["partition_id = ?1".to_string(), revision_filter]; - let mut params_vec: Vec> = vec![Box::new(partition_id.as_str().to_string())]; + let mut params_vec: Vec> = + vec![Box::new(partition_id.as_str().to_string())]; if let Some(ref st) = filter.subject_type { params_vec.push(Box::new(format!("{st}:%"))); @@ -1149,11 +1203,7 @@ impl StorageBackend for SqliteStorage { } let where_clause = conditions.join(" AND "); - let offset = pagination - .cursor - .as_ref() - .map(|c| c.offset) - .unwrap_or(0); + let offset = pagination.cursor.as_ref().map(|c| c.offset).unwrap_or(0); let limit = pagination.limit; let sql = format!( @@ -1191,7 +1241,8 @@ impl StorageBackend for SqliteStorage { .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?; let object = ResourceId::new(&object_str) .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?; - let created_at: DateTime = created_at_str.parse().unwrap_or_else(|_| Utc::now()); + let created_at: DateTime = + created_at_str.parse().unwrap_or_else(|_| Utc::now()); let metadata = metadata_json .and_then(|m| serde_json::from_str::>(&m).ok()); let valid_until = valid_until_str.and_then(|s| s.parse::>().ok()); @@ -1264,7 +1315,10 @@ impl StorageBackend for SqliteStorage { Ok(RevisionToken::new(revision, self.node_id)) } - fn begin_transaction(&self, partition_id: &PartitionId) -> AegisResult> { + fn begin_transaction( + &self, + partition_id: &PartitionId, + ) -> AegisResult> { let _ = partition_id; let conn = self.conn()?; let identity = self.actor_identity.lock().unwrap().clone(); @@ -1282,7 +1336,8 @@ impl StorageBackend for SqliteStorage { ) -> AegisResult> { let conn = self.conn()?; let mut conditions: Vec = vec!["partition_id = ?1".to_string()]; - let mut params_vec: Vec> = vec![Box::new(partition_id.as_str().to_string())]; + let mut params_vec: Vec> = + vec![Box::new(partition_id.as_str().to_string())]; if let Some(obj) = object { params_vec.push(Box::new(obj.as_str().to_string())); @@ -1303,11 +1358,7 @@ impl StorageBackend for SqliteStorage { } else { conditions.join(" AND ") }; - let offset = pagination - .cursor - .as_ref() - .map(|c| c.offset) - .unwrap_or(0); + let offset = pagination.cursor.as_ref().map(|c| c.offset).unwrap_or(0); let limit = pagination.limit; let sql = format!( @@ -1413,10 +1464,16 @@ impl StorageBackend for SqliteStorage { return None; } let wal_path = format!("{}-wal", self.config.path); - std::fs::metadata(&wal_path).ok().map(|m| m.len() as f64 / (1024.0 * 1024.0)) + std::fs::metadata(&wal_path) + .ok() + .map(|m| m.len() as f64 / (1024.0 * 1024.0)) } - fn delete_events_before(&self, partition_id: &PartitionId, cutoff: DateTime) -> AegisResult { + fn delete_events_before( + &self, + partition_id: &PartitionId, + cutoff: DateTime, + ) -> AegisResult { let conn = self.conn()?; let cutoff_str = cutoff.to_rfc3339(); let count = conn @@ -1455,6 +1512,7 @@ impl StorageBackend for SqliteStorage { } fn close(&self) -> AegisResult<()> { + #[allow(clippy::collapsible_if)] if self.config.wal_mode && self.config.path != ":memory:" { if let Ok(conn) = self.pool.get() { let _ = conn.execute_batch("PRAGMA wal_checkpoint(TRUNCATE);"); @@ -1540,7 +1598,11 @@ impl StorageBackend for SqliteStorage { }) } - fn recover_from_events(&self, partition_id: &PartitionId, to_revision: Option) -> AegisResult { + fn recover_from_events( + &self, + partition_id: &PartitionId, + to_revision: Option, + ) -> AegisResult { self.recover_from_events_impl(partition_id, to_revision) } @@ -1569,14 +1631,39 @@ impl StorageBackend for SqliteStorage { let identity: Option = row.get(9)?; let previous_hash: String = row.get(10)?; let event_hash: String = row.get(11)?; - Ok((event_id, revision, action, subject, relation, object, pid, metadata, timestamp, identity, previous_hash, event_hash)) + Ok(( + event_id, + revision, + action, + subject, + relation, + object, + pid, + metadata, + timestamp, + identity, + previous_hash, + event_hash, + )) }) .map_err(|e| AegisError::StorageQuery(e.to_string()))?; let mut last_event_hash = String::new(); for row in rows { - let (event_id, revision, action, subject, relation, object, pid, metadata, timestamp, identity, prev_hash, event_hash) = - row.map_err(|e| AegisError::StorageQuery(e.to_string()))?; + let ( + event_id, + revision, + action, + subject, + relation, + object, + pid, + metadata, + timestamp, + identity, + prev_hash, + event_hash, + ) = row.map_err(|e| AegisError::StorageQuery(e.to_string()))?; if prev_hash != last_event_hash { return Ok(Some(format!( @@ -1699,51 +1786,55 @@ impl StorageBackend for SqliteStorage { ) .map_err(|e| AegisError::StorageQuery(e.to_string()))?; - let result = stmt - .query_row(params![id], |row| { - let id_str: String = row.get(0)?; - let name: String = row.get(1)?; - let description: String = row.get(2)?; - let schema_json: String = row.get(3)?; - let base_version: i64 = row.get(4)?; - let status_str: String = row.get(5)?; - let created_at: String = row.get(6)?; - let updated_at: String = row.get(7)?; - let created_by: String = row.get(8)?; - let approved_by: Option = row.get(9)?; - let rejection_reason: Option = row.get(10)?; - - let schema = serde_json::from_str(&schema_json) - .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?; - let status = match status_str.as_str() { - "drafting" => crate::engine::policy_lifecycle::DraftStatus::Drafting, - "under_review" => crate::engine::policy_lifecycle::DraftStatus::UnderReview, - "approved" => crate::engine::policy_lifecycle::DraftStatus::Approved, - "published" => crate::engine::policy_lifecycle::DraftStatus::Published, - "rejected" => crate::engine::policy_lifecycle::DraftStatus::Rejected, - "superseded" => crate::engine::policy_lifecycle::DraftStatus::Superseded, - "archived" => crate::engine::policy_lifecycle::DraftStatus::Archived, - other => return Err(rusqlite::Error::ToSqlConversionFailure( - Box::new(std::io::Error::new(std::io::ErrorKind::InvalidData, format!("unknown DraftStatus: {}", other))) - )), - }; - let id = uuid::Uuid::parse_str(&id_str) - .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?; - - Ok(PolicyDraft { - id, - name, - description, - schema, - base_version: base_version as u32, - status, - created_at, - updated_at, - created_by, - approved_by, - rejection_reason, - }) - }); + let result = stmt.query_row(params![id], |row| { + let id_str: String = row.get(0)?; + let name: String = row.get(1)?; + let description: String = row.get(2)?; + let schema_json: String = row.get(3)?; + let base_version: i64 = row.get(4)?; + let status_str: String = row.get(5)?; + let created_at: String = row.get(6)?; + let updated_at: String = row.get(7)?; + let created_by: String = row.get(8)?; + let approved_by: Option = row.get(9)?; + let rejection_reason: Option = row.get(10)?; + + let schema = serde_json::from_str(&schema_json) + .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?; + let status = match status_str.as_str() { + "drafting" => crate::engine::policy_lifecycle::DraftStatus::Drafting, + "under_review" => crate::engine::policy_lifecycle::DraftStatus::UnderReview, + "approved" => crate::engine::policy_lifecycle::DraftStatus::Approved, + "published" => crate::engine::policy_lifecycle::DraftStatus::Published, + "rejected" => crate::engine::policy_lifecycle::DraftStatus::Rejected, + "superseded" => crate::engine::policy_lifecycle::DraftStatus::Superseded, + "archived" => crate::engine::policy_lifecycle::DraftStatus::Archived, + other => { + return Err(rusqlite::Error::ToSqlConversionFailure(Box::new( + std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("unknown DraftStatus: {}", other), + ), + ))); + } + }; + let id = uuid::Uuid::parse_str(&id_str) + .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?; + + Ok(PolicyDraft { + id, + name, + description, + schema, + base_version: base_version as u32, + status, + created_at, + updated_at, + created_by, + approved_by, + rejection_reason, + }) + }); match result { Ok(draft) => Ok(Some(draft)), @@ -1767,8 +1858,10 @@ impl StorageBackend for SqliteStorage { let conn = self.conn()?; let queries_json = serde_json::to_string(&schedule.queries) .map_err(|e| AegisError::MetadataValidation(e.to_string()))?; - let compare_schema_json = schedule.compare_schema.as_ref() - .map(|s| serde_json::to_string(s)) + let compare_schema_json = schedule + .compare_schema + .as_ref() + .map(serde_json::to_string) .transpose() .map_err(|e| AegisError::MetadataValidation(e.to_string()))?; conn.execute( @@ -1851,7 +1944,11 @@ impl SqliteStorage { /// Recover the tuple graph from the event log. /// Replays all events in revision order to reconstruct the current state. /// After recovery, verifies that the final revision matches. - fn recover_from_events_impl(&self, partition_id: &PartitionId, to_revision: Option) -> AegisResult { + fn recover_from_events_impl( + &self, + partition_id: &PartitionId, + to_revision: Option, + ) -> AegisResult { self.with_write_tx(|conn| { conn.execute("DELETE FROM _aegis_tuples WHERE partition_id = ?1", [partition_id.as_str()]) .map_err(|e| AegisError::StorageQuery(e.to_string()))?; @@ -1884,6 +1981,7 @@ impl SqliteStorage { row.map_err(|e| AegisError::StorageQuery(e.to_string()))?; let rev = Revision::new(rev as u64); + #[allow(clippy::collapsible_if)] if let Some(target) = to_revision { if rev > target { continue; @@ -1923,7 +2021,7 @@ impl SqliteStorage { .map_err(|e| AegisError::StorageQuery(e.to_string()))?; } - Ok(Self::read_revision(conn)?) + Self::read_revision(conn) }) } @@ -1993,7 +2091,7 @@ impl SqliteStorage { ) .map_err(|e| AegisError::StorageQuery(e.to_string()))?; - Ok(Self::read_revision(conn)?) + Self::read_revision(conn) }) } @@ -2069,7 +2167,11 @@ pub struct SqliteTransaction { } impl SqliteTransaction { - pub fn new(conn: r2d2::PooledConnection, node_id: Uuid, actor_identity: Option) -> AegisResult { + pub fn new( + conn: r2d2::PooledConnection, + node_id: Uuid, + actor_identity: Option, + ) -> AegisResult { conn.execute_batch("BEGIN IMMEDIATE") .map_err(|e| AegisError::StorageQuery(e.to_string()))?; Ok(Self { @@ -2091,6 +2193,7 @@ impl SqliteTransaction { SqliteStorage::bump_revision(conn) } + #[allow(clippy::too_many_arguments)] fn append_event( &self, revision: Revision, @@ -2102,7 +2205,17 @@ impl SqliteTransaction { metadata: Option<&str>, ) -> AegisResult<()> { let conn = self.conn()?; - SqliteStorage::append_event(conn, revision, action, subject, relation, object, partition_id, metadata, self.actor_identity.as_deref()) + SqliteStorage::append_event( + conn, + revision, + action, + subject, + relation, + object, + partition_id, + metadata, + self.actor_identity.as_deref(), + ) } } @@ -2243,9 +2356,10 @@ impl StorageTransaction for SqliteTransaction { } fn commit(mut self: Box) -> AegisResult { - let conn = self.conn.as_ref().ok_or_else(|| { - AegisError::Internal("transaction already consumed".into()) - })?; + let conn = self + .conn + .as_ref() + .ok_or_else(|| AegisError::Internal("transaction already consumed".into()))?; let revision = SqliteStorage::read_revision(conn)?; conn.execute_batch("COMMIT") .map_err(|e| AegisError::StorageQuery(e.to_string()))?; @@ -2255,6 +2369,7 @@ impl StorageTransaction for SqliteTransaction { } fn rollback(mut self: Box) -> AegisResult<()> { + #[allow(clippy::collapsible_if)] if !self.committed { if let Some(conn) = self.conn.take() { conn.execute_batch("ROLLBACK") @@ -2267,6 +2382,7 @@ impl StorageTransaction for SqliteTransaction { impl Drop for SqliteTransaction { fn drop(&mut self) { + #[allow(clippy::collapsible_if)] if !self.committed { if let Some(conn) = self.conn.take() { let _ = conn.execute_batch("ROLLBACK"); @@ -2316,10 +2432,14 @@ mod tests { let meta = store.initialize().unwrap(); assert!(meta.healthy); - let rev = store.write_tuple(&PartitionId::default(), &test_tuple()).unwrap(); + let rev = store + .write_tuple(&PartitionId::default(), &test_tuple()) + .unwrap(); assert!(rev.as_u64() > 0); - let has = store.has_tuple(&PartitionId::default(), &test_tuple().key()).unwrap(); + let has = store + .has_tuple(&PartitionId::default(), &test_tuple().key()) + .unwrap(); assert!(has); } @@ -2328,9 +2448,13 @@ mod tests { let mut store = storage(); store.initialize().unwrap(); - store.write_tuple(&PartitionId::default(), &test_tuple()).unwrap(); + store + .write_tuple(&PartitionId::default(), &test_tuple()) + .unwrap(); - let read = store.read_tuple(&PartitionId::default(), &test_tuple().key()).unwrap(); + let read = store + .read_tuple(&PartitionId::default(), &test_tuple().key()) + .unwrap(); assert!(read.is_some()); let t = read.unwrap(); assert_eq!(t.subject.as_str(), "user:123"); @@ -2343,8 +2467,15 @@ mod tests { let mut store = storage(); store.initialize().unwrap(); - let r1 = store.write_tuple(&PartitionId::default(), &test_tuple()).unwrap(); - let r2 = store.write_tuple(&PartitionId::default(), &tuple("user:456", "viewer", "repo:other")).unwrap(); + let r1 = store + .write_tuple(&PartitionId::default(), &test_tuple()) + .unwrap(); + let r2 = store + .write_tuple( + &PartitionId::default(), + &tuple("user:456", "viewer", "repo:other"), + ) + .unwrap(); assert_eq!(r1.as_u64() + 1, r2.as_u64()); } @@ -2353,8 +2484,12 @@ mod tests { let mut store = storage(); store.initialize().unwrap(); - store.write_tuple(&PartitionId::default(), &test_tuple()).unwrap(); - store.write_tuple(&PartitionId::default(), &test_tuple()).unwrap(); // same tuple again + store + .write_tuple(&PartitionId::default(), &test_tuple()) + .unwrap(); + store + .write_tuple(&PartitionId::default(), &test_tuple()) + .unwrap(); // same tuple again let count = store .conn() @@ -2375,11 +2510,23 @@ mod tests { let mut store = storage(); store.initialize().unwrap(); - store.write_tuple(&PartitionId::default(), &test_tuple()).unwrap(); - assert!(store.has_tuple(&PartitionId::default(), &test_tuple().key()).unwrap()); + store + .write_tuple(&PartitionId::default(), &test_tuple()) + .unwrap(); + assert!( + store + .has_tuple(&PartitionId::default(), &test_tuple().key()) + .unwrap() + ); - store.delete_tuple(&PartitionId::default(), &test_tuple().key()).unwrap(); - assert!(!store.has_tuple(&PartitionId::default(), &test_tuple().key()).unwrap()); + store + .delete_tuple(&PartitionId::default(), &test_tuple().key()) + .unwrap(); + assert!( + !store + .has_tuple(&PartitionId::default(), &test_tuple().key()) + .unwrap() + ); } #[test] @@ -2389,7 +2536,10 @@ mod tests { let rev_before = store.current_revision(&PartitionId::default()).unwrap(); let rev_after = store - .delete_tuple(&PartitionId::default(), &key("user:999", "editor", "repo:nonexistent")) + .delete_tuple( + &PartitionId::default(), + &key("user:999", "editor", "repo:nonexistent"), + ) .unwrap(); assert_eq!(rev_before, rev_after); // no bump } @@ -2399,12 +2549,27 @@ mod tests { let mut store = storage(); store.initialize().unwrap(); - store.write_tuple(&PartitionId::default(), &tuple("user:1", "editor", "repo:a")).unwrap(); - store.write_tuple(&PartitionId::default(), &tuple("user:1", "viewer", "repo:b")).unwrap(); + store + .write_tuple( + &PartitionId::default(), + &tuple("user:1", "editor", "repo:a"), + ) + .unwrap(); + store + .write_tuple( + &PartitionId::default(), + &tuple("user:1", "viewer", "repo:b"), + ) + .unwrap(); assert_eq!( store - .list_by_subject(&PartitionId::default(), &SubjectId::new("user:1").unwrap(), None, &ConsistencyMode::MinimizeLatency) + .list_by_subject( + &PartitionId::default(), + &SubjectId::new("user:1").unwrap(), + None, + &ConsistencyMode::MinimizeLatency + ) .unwrap() .len(), 2 @@ -2416,7 +2581,12 @@ mod tests { assert_eq!( store - .list_by_subject(&PartitionId::default(), &SubjectId::new("user:1").unwrap(), None, &ConsistencyMode::MinimizeLatency) + .list_by_subject( + &PartitionId::default(), + &SubjectId::new("user:1").unwrap(), + None, + &ConsistencyMode::MinimizeLatency + ) .unwrap() .len(), 0 @@ -2428,12 +2598,27 @@ mod tests { let mut store = storage(); store.initialize().unwrap(); - store.write_tuple(&PartitionId::default(), &tuple("user:1", "editor", "repo:a")).unwrap(); - store.write_tuple(&PartitionId::default(), &tuple("user:2", "viewer", "repo:a")).unwrap(); + store + .write_tuple( + &PartitionId::default(), + &tuple("user:1", "editor", "repo:a"), + ) + .unwrap(); + store + .write_tuple( + &PartitionId::default(), + &tuple("user:2", "viewer", "repo:a"), + ) + .unwrap(); assert_eq!( store - .list_by_object(&PartitionId::default(), &ResourceId::new("repo:a").unwrap(), None, &ConsistencyMode::MinimizeLatency) + .list_by_object( + &PartitionId::default(), + &ResourceId::new("repo:a").unwrap(), + None, + &ConsistencyMode::MinimizeLatency + ) .unwrap() .len(), 2 @@ -2445,7 +2630,12 @@ mod tests { assert_eq!( store - .list_by_object(&PartitionId::default(), &ResourceId::new("repo:a").unwrap(), None, &ConsistencyMode::MinimizeLatency) + .list_by_object( + &PartitionId::default(), + &ResourceId::new("repo:a").unwrap(), + None, + &ConsistencyMode::MinimizeLatency + ) .unwrap() .len(), 0 @@ -2459,11 +2649,26 @@ mod tests { let mut store = storage(); store.initialize().unwrap(); - store.write_tuple(&PartitionId::default(), &tuple("user:1", "editor", "repo:a")).unwrap(); - store.write_tuple(&PartitionId::default(), &tuple("user:2", "viewer", "repo:a")).unwrap(); + store + .write_tuple( + &PartitionId::default(), + &tuple("user:1", "editor", "repo:a"), + ) + .unwrap(); + store + .write_tuple( + &PartitionId::default(), + &tuple("user:2", "viewer", "repo:a"), + ) + .unwrap(); let results = store - .list_by_object(&PartitionId::default(), &ResourceId::new("repo:a").unwrap(), None, &ConsistencyMode::MinimizeLatency) + .list_by_object( + &PartitionId::default(), + &ResourceId::new("repo:a").unwrap(), + None, + &ConsistencyMode::MinimizeLatency, + ) .unwrap(); assert_eq!(results.len(), 2); } @@ -2473,8 +2678,18 @@ mod tests { let mut store = storage(); store.initialize().unwrap(); - store.write_tuple(&PartitionId::default(), &tuple("user:1", "editor", "repo:a")).unwrap(); - store.write_tuple(&PartitionId::default(), &tuple("user:2", "viewer", "repo:a")).unwrap(); + store + .write_tuple( + &PartitionId::default(), + &tuple("user:1", "editor", "repo:a"), + ) + .unwrap(); + store + .write_tuple( + &PartitionId::default(), + &tuple("user:2", "viewer", "repo:a"), + ) + .unwrap(); let results = store .list_by_object( @@ -2493,11 +2708,26 @@ mod tests { let mut store = storage(); store.initialize().unwrap(); - store.write_tuple(&PartitionId::default(), &tuple("user:1", "editor", "repo:a")).unwrap(); - store.write_tuple(&PartitionId::default(), &tuple("user:1", "viewer", "repo:b")).unwrap(); + store + .write_tuple( + &PartitionId::default(), + &tuple("user:1", "editor", "repo:a"), + ) + .unwrap(); + store + .write_tuple( + &PartitionId::default(), + &tuple("user:1", "viewer", "repo:b"), + ) + .unwrap(); let results = store - .list_by_subject(&PartitionId::default(), &SubjectId::new("user:1").unwrap(), None, &ConsistencyMode::MinimizeLatency) + .list_by_subject( + &PartitionId::default(), + &SubjectId::new("user:1").unwrap(), + None, + &ConsistencyMode::MinimizeLatency, + ) .unwrap(); assert_eq!(results.len(), 2); } @@ -2507,9 +2737,24 @@ mod tests { let mut store = storage(); store.initialize().unwrap(); - store.write_tuple(&PartitionId::default(), &tuple("user:1", "editor", "repo:a")).unwrap(); - store.write_tuple(&PartitionId::default(), &tuple("user:2", "editor", "repo:a")).unwrap(); - store.write_tuple(&PartitionId::default(), &tuple("user:3", "viewer", "repo:a")).unwrap(); + store + .write_tuple( + &PartitionId::default(), + &tuple("user:1", "editor", "repo:a"), + ) + .unwrap(); + store + .write_tuple( + &PartitionId::default(), + &tuple("user:2", "editor", "repo:a"), + ) + .unwrap(); + store + .write_tuple( + &PartitionId::default(), + &tuple("user:3", "viewer", "repo:a"), + ) + .unwrap(); let results = store .list_by_relation( @@ -2530,11 +2775,10 @@ mod tests { for i in 0..10 { store - .write_tuple(&PartitionId::default(), &tuple( - &format!("user:{i}"), - "editor", - "repo:fluxbus", - )) + .write_tuple( + &PartitionId::default(), + &tuple(&format!("user:{i}"), "editor", "repo:fluxbus"), + ) .unwrap(); } @@ -2598,15 +2842,30 @@ mod tests { let mut store = storage(); store.initialize().unwrap(); - store.write_tuple(&PartitionId::default(), &tuple("user:1", "editor", "repo:a")).unwrap(); - store.write_tuple(&PartitionId::default(), &tuple("user:2", "editor", "repo:a")).unwrap(); + store + .write_tuple( + &PartitionId::default(), + &tuple("user:1", "editor", "repo:a"), + ) + .unwrap(); + store + .write_tuple( + &PartitionId::default(), + &tuple("user:2", "editor", "repo:a"), + ) + .unwrap(); let filter = TupleFilter { subject_type: Some("user".to_string()), ..Default::default() }; let results = store - .query_tuples(&PartitionId::default(), &filter, &PaginationParams::default(), &ConsistencyMode::MinimizeLatency) + .query_tuples( + &PartitionId::default(), + &filter, + &PaginationParams::default(), + &ConsistencyMode::MinimizeLatency, + ) .unwrap(); assert_eq!(results.tuples.len(), 2); } @@ -2618,13 +2877,38 @@ mod tests { let mut store = storage(); store.initialize().unwrap(); - assert_eq!(store.current_revision(&PartitionId::default()).unwrap().as_u64(), 0); + assert_eq!( + store + .current_revision(&PartitionId::default()) + .unwrap() + .as_u64(), + 0 + ); - store.write_tuple(&PartitionId::default(), &test_tuple()).unwrap(); - assert_eq!(store.current_revision(&PartitionId::default()).unwrap().as_u64(), 1); + store + .write_tuple(&PartitionId::default(), &test_tuple()) + .unwrap(); + assert_eq!( + store + .current_revision(&PartitionId::default()) + .unwrap() + .as_u64(), + 1 + ); - store.write_tuple(&PartitionId::default(), &tuple("user:456", "viewer", "repo:other")).unwrap(); - assert_eq!(store.current_revision(&PartitionId::default()).unwrap().as_u64(), 2); + store + .write_tuple( + &PartitionId::default(), + &tuple("user:456", "viewer", "repo:other"), + ) + .unwrap(); + assert_eq!( + store + .current_revision(&PartitionId::default()) + .unwrap() + .as_u64(), + 2 + ); } #[test] @@ -2646,11 +2930,19 @@ mod tests { let mut tx = store.begin_transaction(&PartitionId::default()).unwrap(); tx.write(&PartitionId::default(), &test_tuple()).unwrap(); - tx.write(&PartitionId::default(), &tuple("user:456", "viewer", "repo:other")).unwrap(); + tx.write( + &PartitionId::default(), + &tuple("user:456", "viewer", "repo:other"), + ) + .unwrap(); let rev = tx.commit().unwrap(); assert!(rev.as_u64() > 0); - assert!(store.has_tuple(&PartitionId::default(), &test_tuple().key()).unwrap()); + assert!( + store + .has_tuple(&PartitionId::default(), &test_tuple().key()) + .unwrap() + ); } #[test] @@ -2664,8 +2956,15 @@ mod tests { tx.write(&PartitionId::default(), &test_tuple()).unwrap(); tx.rollback().unwrap(); - assert_eq!(store.current_revision(&PartitionId::default()).unwrap(), rev_before); - assert!(!store.has_tuple(&PartitionId::default(), &test_tuple().key()).unwrap()); + assert_eq!( + store.current_revision(&PartitionId::default()).unwrap(), + rev_before + ); + assert!( + !store + .has_tuple(&PartitionId::default(), &test_tuple().key()) + .unwrap() + ); } #[test] @@ -2677,7 +2976,11 @@ mod tests { tx.write(&PartitionId::default(), &test_tuple()).unwrap(); tx.savepoint("sp1").unwrap(); - tx.write(&PartitionId::default(), &tuple("user:savepoint", "test", "repo:sp")).unwrap(); + tx.write( + &PartitionId::default(), + &tuple("user:savepoint", "test", "repo:sp"), + ) + .unwrap(); // Savepoint tuple should exist (it was written after the savepoint) tx.rollback_to_savepoint("sp1").unwrap(); @@ -2687,8 +2990,19 @@ mod tests { assert!(rev.as_u64() > 0); // After commit: only the original tuple exists, savepoint tuple was rolled back - assert!(store.has_tuple(&PartitionId::default(), &test_tuple().key()).unwrap()); - assert!(!store.has_tuple(&PartitionId::default(), &key("user:savepoint", "test", "repo:sp")).unwrap()); + assert!( + store + .has_tuple(&PartitionId::default(), &test_tuple().key()) + .unwrap() + ); + assert!( + !store + .has_tuple( + &PartitionId::default(), + &key("user:savepoint", "test", "repo:sp") + ) + .unwrap() + ); } #[test] @@ -2704,7 +3018,10 @@ mod tests { // tx drops without commit } - assert_eq!(store.current_revision(&PartitionId::default()).unwrap(), rev_before); + assert_eq!( + store.current_revision(&PartitionId::default()).unwrap(), + rev_before + ); } // ── Audit ── @@ -2714,7 +3031,9 @@ mod tests { let mut store = storage(); store.initialize().unwrap(); - store.write_tuple(&PartitionId::default(), &test_tuple()).unwrap(); + store + .write_tuple(&PartitionId::default(), &test_tuple()) + .unwrap(); store .delete_tuple(&PartitionId::default(), &test_tuple().key()) .unwrap(); @@ -2739,10 +3058,16 @@ mod tests { store.initialize().unwrap(); store - .write_tuple(&PartitionId::default(), &tuple("user:1", "editor", "repo:a")) + .write_tuple( + &PartitionId::default(), + &tuple("user:1", "editor", "repo:a"), + ) .unwrap(); let r2 = store - .write_tuple(&PartitionId::default(), &tuple("user:2", "viewer", "repo:a")) + .write_tuple( + &PartitionId::default(), + &tuple("user:2", "viewer", "repo:a"), + ) .unwrap(); let audit = store @@ -2774,11 +3099,17 @@ mod tests { let mut store = storage(); store.initialize().unwrap(); - store.write_tuple(&PartitionId::default(), &test_tuple()).unwrap(); + store + .write_tuple(&PartitionId::default(), &test_tuple()) + .unwrap(); store.close().unwrap(); // After close, can still read (pool connections may be live) - assert!(store.has_tuple(&PartitionId::default(), &test_tuple().key()).unwrap()); + assert!( + store + .has_tuple(&PartitionId::default(), &test_tuple().key()) + .unwrap() + ); } // ── Revision Snapshots ── @@ -2789,17 +3120,34 @@ mod tests { store.initialize().unwrap(); // Write tuple at rev 1 - store.write_tuple(&PartitionId::default(), &tuple("user:1", "editor", "repo:a")).unwrap(); + store + .write_tuple( + &PartitionId::default(), + &tuple("user:1", "editor", "repo:a"), + ) + .unwrap(); let rev_before_delete = store.current_revision(&PartitionId::default()).unwrap(); // Delete and re-write at rev 2+ - store.write_tuple(&PartitionId::default(), &tuple("user:2", "viewer", "repo:a")).unwrap(); - store.delete_tuple(&PartitionId::default(), &key("user:1", "editor", "repo:a")).unwrap(); + store + .write_tuple( + &PartitionId::default(), + &tuple("user:2", "viewer", "repo:a"), + ) + .unwrap(); + store + .delete_tuple(&PartitionId::default(), &key("user:1", "editor", "repo:a")) + .unwrap(); // Read at rev 1 should see user:1 only (active at that point) let conn = store.conn().unwrap(); - let at_rev1 = SqliteStorage::read_tuples_at_revision(&conn, rev_before_delete.as_u64() as i64).unwrap(); - let subjects_at_rev1: Vec = at_rev1.iter().map(|t| t.subject.as_str().to_string()).collect(); + let at_rev1 = + SqliteStorage::read_tuples_at_revision(&conn, rev_before_delete.as_u64() as i64) + .unwrap(); + let subjects_at_rev1: Vec = at_rev1 + .iter() + .map(|t| t.subject.as_str().to_string()) + .collect(); assert!(subjects_at_rev1.contains(&"user:1".to_string())); assert!(!subjects_at_rev1.contains(&"user:2".to_string())); } @@ -2817,14 +3165,27 @@ mod tests { tuple("team:eng", "owner", "workspace:core"), ]; - let rev = store.write_tuples_batch(&PartitionId::default(), &tuples).unwrap(); + let rev = store + .write_tuples_batch(&PartitionId::default(), &tuples) + .unwrap(); assert!(rev.as_u64() > 0); - assert!(store.has_tuple(&PartitionId::default(), &key("user:1", "editor", "repo:a")).unwrap()); - assert!(store.has_tuple(&PartitionId::default(), &key("user:2", "viewer", "repo:b")).unwrap()); assert!( store - .has_tuple(&PartitionId::default(), &key("team:eng", "owner", "workspace:core")) + .has_tuple(&PartitionId::default(), &key("user:1", "editor", "repo:a")) + .unwrap() + ); + assert!( + store + .has_tuple(&PartitionId::default(), &key("user:2", "viewer", "repo:b")) + .unwrap() + ); + assert!( + store + .has_tuple( + &PartitionId::default(), + &key("team:eng", "owner", "workspace:core") + ) .unwrap() ); } @@ -2834,7 +3195,9 @@ mod tests { let mut store = storage(); store.initialize().unwrap(); - let rev = store.write_tuples_batch(&PartitionId::default(), &[]).unwrap(); + let rev = store + .write_tuples_batch(&PartitionId::default(), &[]) + .unwrap(); assert_eq!(rev.as_u64(), 0); } @@ -2858,7 +3221,10 @@ mod tests { store.write_tuple(&PartitionId::default(), &tuple).unwrap(); - let read = store.read_tuple(&PartitionId::default(), &tuple.key()).unwrap().unwrap(); + let read = store + .read_tuple(&PartitionId::default(), &tuple.key()) + .unwrap() + .unwrap(); assert_eq!(read.metadata.unwrap(), meta); } @@ -2878,8 +3244,14 @@ mod tests { // In in-memory mode, WAL may not be used, but we verify no crash let mut store = store; store.initialize().unwrap(); - store.write_tuple(&PartitionId::default(), &test_tuple()).unwrap(); - assert!(store.has_tuple(&PartitionId::default(), &test_tuple().key()).unwrap()); + store + .write_tuple(&PartitionId::default(), &test_tuple()) + .unwrap(); + assert!( + store + .has_tuple(&PartitionId::default(), &test_tuple().key()) + .unwrap() + ); } // ── Initialize ── @@ -2901,15 +3273,19 @@ mod tests { let store = storage(); assert!( store - .list_by_object(&PartitionId::default(), &ResourceId::new("nonexistent").unwrap(), None, &ConsistencyMode::MinimizeLatency) + .list_by_object( + &PartitionId::default(), + &ResourceId::new("nonexistent").unwrap(), + None, + &ConsistencyMode::MinimizeLatency + ) .unwrap() .is_empty() ); assert!( - store + !store .has_tuple(&PartitionId::default(), &key("user:1", "editor", "repo:a")) .unwrap() - == false ); } @@ -2920,15 +3296,35 @@ mod tests { let mut store = storage(); store.initialize().unwrap(); - store.write_tuple(&PartitionId::default(), &tuple("user:1", "editor", "repo:a")).unwrap(); - store.write_tuple(&PartitionId::default(), &tuple("user:2", "viewer", "repo:b")).unwrap(); + store + .write_tuple( + &PartitionId::default(), + &tuple("user:1", "editor", "repo:a"), + ) + .unwrap(); + store + .write_tuple( + &PartitionId::default(), + &tuple("user:2", "viewer", "repo:b"), + ) + .unwrap(); let rev_before = store.current_revision(&PartitionId::default()).unwrap(); - let recovered = store.recover_from_events(&PartitionId::default(), None).unwrap(); + let recovered = store + .recover_from_events(&PartitionId::default(), None) + .unwrap(); assert_eq!(recovered, rev_before); - assert!(store.has_tuple(&PartitionId::default(), &key("user:1", "editor", "repo:a")).unwrap()); - assert!(store.has_tuple(&PartitionId::default(), &key("user:2", "viewer", "repo:b")).unwrap()); + assert!( + store + .has_tuple(&PartitionId::default(), &key("user:1", "editor", "repo:a")) + .unwrap() + ); + assert!( + store + .has_tuple(&PartitionId::default(), &key("user:2", "viewer", "repo:b")) + .unwrap() + ); } #[test] @@ -2936,14 +3332,32 @@ mod tests { let mut store = storage(); store.initialize().unwrap(); - store.write_tuple(&PartitionId::default(), &tuple("user:1", "editor", "repo:a")).unwrap(); - store.write_tuple(&PartitionId::default(), &tuple("user:2", "viewer", "repo:b")).unwrap(); + store + .write_tuple( + &PartitionId::default(), + &tuple("user:1", "editor", "repo:a"), + ) + .unwrap(); + store + .write_tuple( + &PartitionId::default(), + &tuple("user:2", "viewer", "repo:b"), + ) + .unwrap(); let recovered = store.recover_to_revision(Revision::new(1)).unwrap(); assert_eq!(recovered.as_u64(), 1); - assert!(store.has_tuple(&PartitionId::default(), &key("user:1", "editor", "repo:a")).unwrap()); - assert!(!store.has_tuple(&PartitionId::default(), &key("user:2", "viewer", "repo:b")).unwrap()); + assert!( + store + .has_tuple(&PartitionId::default(), &key("user:1", "editor", "repo:a")) + .unwrap() + ); + assert!( + !store + .has_tuple(&PartitionId::default(), &key("user:2", "viewer", "repo:b")) + .unwrap() + ); } #[test] @@ -2951,11 +3365,22 @@ mod tests { let mut store = storage(); store.initialize().unwrap(); - store.write_tuple(&PartitionId::default(), &tuple("user:1", "editor", "repo:a")).unwrap(); - store.delete_tuple(&PartitionId::default(), &key("user:1", "editor", "repo:a")).unwrap(); + store + .write_tuple( + &PartitionId::default(), + &tuple("user:1", "editor", "repo:a"), + ) + .unwrap(); + store + .delete_tuple(&PartitionId::default(), &key("user:1", "editor", "repo:a")) + .unwrap(); - let before_events = store.conn().unwrap() - .query_row("SELECT COUNT(*) FROM _aegis_events", [], |row| row.get::<_, i64>(0)) + let before_events = store + .conn() + .unwrap() + .query_row("SELECT COUNT(*) FROM _aegis_events", [], |row| { + row.get::<_, i64>(0) + }) .unwrap(); assert_eq!(before_events, 2); @@ -2963,8 +3388,12 @@ mod tests { let removed = store.compact_events(&PartitionId::default()).unwrap(); assert_eq!(removed, 2); - let after_events = store.conn().unwrap() - .query_row("SELECT COUNT(*) FROM _aegis_events", [], |row| row.get::<_, i64>(0)) + let after_events = store + .conn() + .unwrap() + .query_row("SELECT COUNT(*) FROM _aegis_events", [], |row| { + row.get::<_, i64>(0) + }) .unwrap(); assert_eq!(after_events, 0); } @@ -2974,7 +3403,9 @@ mod tests { let mut store = storage(); store.initialize().unwrap(); - let recovered = store.recover_from_events(&PartitionId::default(), None).unwrap(); + let recovered = store + .recover_from_events(&PartitionId::default(), None) + .unwrap(); assert_eq!(recovered.as_u64(), 0); } @@ -2993,29 +3424,61 @@ mod tests { // Empty name let tx = store.begin_transaction(&PartitionId::default()).unwrap(); let err = tx.savepoint("").unwrap_err(); - assert!(matches!(err, AegisError::Validation(crate::types::ValidationError::Empty)), "empty name should fail: {err}"); + assert!( + matches!( + err, + AegisError::Validation(crate::types::ValidationError::Empty) + ), + "empty name should fail: {err}" + ); tx.rollback().ok(); // Too long name (65 chars) let tx = store.begin_transaction(&PartitionId::default()).unwrap(); let long_name = "a".repeat(65); let err = tx.savepoint(&long_name).unwrap_err(); - assert!(matches!(err, AegisError::Validation(crate::types::ValidationError::TooLong { .. })), "long name should fail: {err}"); + assert!( + matches!( + err, + AegisError::Validation(crate::types::ValidationError::TooLong { .. }) + ), + "long name should fail: {err}" + ); tx.rollback().ok(); // Invalid characters (SQL injection attempt) let tx = store.begin_transaction(&PartitionId::default()).unwrap(); - let err = tx.savepoint("\"; DROP TABLE _aegis_tuples; --").unwrap_err(); - assert!(matches!(err, AegisError::Validation(crate::types::ValidationError::InvalidCharacters(_))), "injection attempt should fail: {err}"); + let err = tx + .savepoint("\"; DROP TABLE _aegis_tuples; --") + .unwrap_err(); + assert!( + matches!( + err, + AegisError::Validation(crate::types::ValidationError::InvalidCharacters(_)) + ), + "injection attempt should fail: {err}" + ); tx.rollback().ok(); // Same validation applies to rollback_to_savepoint and release_savepoint let tx = store.begin_transaction(&PartitionId::default()).unwrap(); tx.savepoint("valid").unwrap(); let err = tx.rollback_to_savepoint("invalid!").unwrap_err(); - assert!(matches!(err, AegisError::Validation(crate::types::ValidationError::InvalidCharacters(_))), "rollback_to_savepoint should validate name: {err}"); + assert!( + matches!( + err, + AegisError::Validation(crate::types::ValidationError::InvalidCharacters(_)) + ), + "rollback_to_savepoint should validate name: {err}" + ); let err = tx.release_savepoint("no space").unwrap_err(); - assert!(matches!(err, AegisError::Validation(crate::types::ValidationError::InvalidCharacters(_))), "release_savepoint should validate name: {err}"); + assert!( + matches!( + err, + AegisError::Validation(crate::types::ValidationError::InvalidCharacters(_)) + ), + "release_savepoint should validate name: {err}" + ); tx.rollback().ok(); } } diff --git a/crates/aegis-core/src/storage/traits.rs b/crates/aegis-core/src/storage/traits.rs index 1027f03..d43bace 100644 --- a/crates/aegis-core/src/storage/traits.rs +++ b/crates/aegis-core/src/storage/traits.rs @@ -1,5 +1,3 @@ -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; use crate::engine::enforcement_history::EnforcementEvent; use crate::engine::policy_lifecycle::PolicyDraft; use crate::engine::scheduler::{AnalysisRun, AnalysisSchedule}; @@ -8,6 +6,8 @@ use crate::types::{ AuditEntry, ConnectionStats, ConsistencyMode, PaginatedTuples, PaginationParams, PartitionId, Relation, RelationshipTuple, ResourceId, Revision, RevisionToken, SubjectId, TupleKey, }; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; /// Compute a deterministic SHA-256 hash for an audit event. @@ -15,6 +15,7 @@ use sha2::{Digest, Sha256}; /// `event_hash = sha256(previous_hash || revision(le) || action || subject || relation || object || partition_id || metadata || timestamp || identity)` /// /// The genesis event has `previous_hash = ""`. +#[allow(clippy::too_many_arguments)] pub fn compute_event_hash( previous_hash: &str, revision: i64, @@ -29,7 +30,7 @@ pub fn compute_event_hash( ) -> String { let mut hasher = Sha256::new(); hasher.update(previous_hash.as_bytes()); - hasher.update(&revision.to_le_bytes()); + hasher.update(revision.to_le_bytes()); hasher.update(action.as_bytes()); hasher.update(subject.as_bytes()); hasher.update(relation.as_bytes()); @@ -61,25 +62,45 @@ pub trait StorageBackend: Send + Sync { /// Write a single relationship tuple within a partition. /// Returns the new revision number. - fn write_tuple(&self, partition_id: &PartitionId, tuple: &RelationshipTuple) -> AegisResult; + fn write_tuple( + &self, + partition_id: &PartitionId, + tuple: &RelationshipTuple, + ) -> AegisResult; /// Write multiple tuples atomically within a single transaction in a partition. - fn write_tuples_batch(&self, partition_id: &PartitionId, tuples: &[RelationshipTuple]) -> AegisResult; + fn write_tuples_batch( + &self, + partition_id: &PartitionId, + tuples: &[RelationshipTuple], + ) -> AegisResult; /// Delete a single relationship tuple by key within a partition. fn delete_tuple(&self, partition_id: &PartitionId, key: &TupleKey) -> AegisResult; /// Delete all tuples for a given subject within a partition. - fn delete_subject(&self, partition_id: &PartitionId, subject: &SubjectId) -> AegisResult; + fn delete_subject( + &self, + partition_id: &PartitionId, + subject: &SubjectId, + ) -> AegisResult; /// Delete all tuples for a given resource within a partition. - fn delete_object(&self, partition_id: &PartitionId, object: &ResourceId) -> AegisResult; + fn delete_object( + &self, + partition_id: &PartitionId, + object: &ResourceId, + ) -> AegisResult; /// Check if a tuple exists within a partition. fn has_tuple(&self, partition_id: &PartitionId, key: &TupleKey) -> AegisResult; /// Read a single tuple by key within a partition. - fn read_tuple(&self, partition_id: &PartitionId, key: &TupleKey) -> AegisResult>; + fn read_tuple( + &self, + partition_id: &PartitionId, + key: &TupleKey, + ) -> AegisResult>; /// List all tuples for a given object within a partition. fn list_by_object( @@ -124,7 +145,10 @@ pub trait StorageBackend: Send + Sync { relation: relation.cloned(), ..Default::default() }, - &PaginationParams { cursor: None, limit: 10_000 }, + &PaginationParams { + cursor: None, + limit: 10_000, + }, _consistency, )?; Ok(all.tuples) @@ -161,7 +185,10 @@ pub trait StorageBackend: Send + Sync { fn current_token(&self) -> AegisResult; /// Begin a transaction. Returns a transaction handle. - fn begin_transaction(&self, partition_id: &PartitionId) -> AegisResult>; + fn begin_transaction( + &self, + partition_id: &PartitionId, + ) -> AegisResult>; /// Query audit log for a given object (or all objects if None) within a partition. fn query_audit( @@ -181,7 +208,11 @@ pub trait StorageBackend: Send + Sync { /// Delete audit events older than the given cutoff timestamp within a partition. /// Returns the number of deleted events. - fn delete_events_before(&self, partition_id: &PartitionId, _cutoff: DateTime) -> AegisResult; + fn delete_events_before( + &self, + partition_id: &PartitionId, + _cutoff: DateTime, + ) -> AegisResult; /// Compact paired add/remove events to reduce audit log size. /// Only meaningful for backends that track individual events (SQLite, PostgreSQL). @@ -191,12 +222,20 @@ pub trait StorageBackend: Send + Sync { /// Permanently remove soft-deleted tuples whose deletion revision /// corresponds to a timestamp before the given cutoff within a partition. /// Returns the number of deleted tuples. - fn delete_soft_deleted_tuples_before(&self, partition_id: &PartitionId, _cutoff: DateTime) -> AegisResult; + fn delete_soft_deleted_tuples_before( + &self, + partition_id: &PartitionId, + _cutoff: DateTime, + ) -> AegisResult; /// Recover the current state by replaying all logged events within a partition. /// This reconstructs the tuple store from scratch using the event log, /// returning the latest revision seen. - fn recover_from_events(&self, partition_id: &PartitionId, to_revision: Option) -> AegisResult; + fn recover_from_events( + &self, + partition_id: &PartitionId, + to_revision: Option, + ) -> AegisResult; /// Restore tuples, events, and revision from a backup in a single transaction. /// Clears existing data in the partition first. @@ -261,37 +300,51 @@ pub trait StorageBackend: Send + Sync { /// Save a policy draft to storage. fn save_policy_draft(&self, _draft: &PolicyDraft) -> AegisResult<()> { - Err(AegisError::UnsupportedStorageOperation("save_policy_draft not supported by this backend".into())) + Err(AegisError::UnsupportedStorageOperation( + "save_policy_draft not supported by this backend".into(), + )) } /// Load a policy draft by ID from storage. fn load_policy_draft(&self, _id: &str) -> AegisResult> { - Err(AegisError::UnsupportedStorageOperation("load_policy_draft not supported by this backend".into())) + Err(AegisError::UnsupportedStorageOperation( + "load_policy_draft not supported by this backend".into(), + )) } /// Delete a policy draft from storage. fn delete_policy_draft(&self, _id: &str) -> AegisResult { - Err(AegisError::UnsupportedStorageOperation("delete_policy_draft not supported by this backend".into())) + Err(AegisError::UnsupportedStorageOperation( + "delete_policy_draft not supported by this backend".into(), + )) } /// Save an analysis schedule to storage. fn save_analysis_schedule(&self, _schedule: &AnalysisSchedule) -> AegisResult<()> { - Err(AegisError::UnsupportedStorageOperation("save_analysis_schedule not supported by this backend".into())) + Err(AegisError::UnsupportedStorageOperation( + "save_analysis_schedule not supported by this backend".into(), + )) } /// Delete an analysis schedule from storage. fn delete_analysis_schedule(&self, _id: &str) -> AegisResult { - Err(AegisError::UnsupportedStorageOperation("delete_analysis_schedule not supported by this backend".into())) + Err(AegisError::UnsupportedStorageOperation( + "delete_analysis_schedule not supported by this backend".into(), + )) } /// Save an analysis run to storage. fn save_analysis_run(&self, _run: &AnalysisRun) -> AegisResult<()> { - Err(AegisError::UnsupportedStorageOperation("save_analysis_run not supported by this backend".into())) + Err(AegisError::UnsupportedStorageOperation( + "save_analysis_run not supported by this backend".into(), + )) } /// Save an enforcement event to storage. fn save_enforcement_event(&self, _event: &EnforcementEvent) -> AegisResult<()> { - Err(AegisError::UnsupportedStorageOperation("save_enforcement_event not supported by this backend".into())) + Err(AegisError::UnsupportedStorageOperation( + "save_enforcement_event not supported by this backend".into(), + )) } } diff --git a/crates/aegis-core/src/telemetry.rs b/crates/aegis-core/src/telemetry.rs index 7cdb310..0cbd3ed 100644 --- a/crates/aegis-core/src/telemetry.rs +++ b/crates/aegis-core/src/telemetry.rs @@ -28,6 +28,7 @@ pub(crate) static METRIC_GRAPH_TUPLE_COUNT: AtomicU64 = AtomicU64::new(0); #[cfg(feature = "telemetry")] pub(crate) static METRIC_GRAPH_TENANT_COUNT: AtomicU64 = AtomicU64::new(0); #[cfg(feature = "telemetry")] +#[allow(dead_code)] pub(crate) static METRIC_GRAPH_PARTITION_COUNT: AtomicU64 = AtomicU64::new(0); #[cfg(feature = "telemetry")] pub(crate) static METRIC_STORAGE_CONNECTIONS_ACTIVE: AtomicU64 = AtomicU64::new(0); @@ -80,8 +81,8 @@ pub fn init_logger() -> TelemetryGuard { #[cfg(feature = "telemetry")] pub fn init_otel() -> Result> { use opentelemetry::global; - use opentelemetry_sdk::trace::SdkTracerProvider; use opentelemetry_sdk::Resource; + use opentelemetry_sdk::trace::SdkTracerProvider; let exporter = opentelemetry_otlp::SpanExporter::builder() .with_tonic() @@ -168,13 +169,13 @@ pub fn update_revision_current(val: u64) { #[cfg(feature = "telemetry")] pub mod otel_metrics { //! OpenTelemetry metric instruments, available only with the `telemetry` feature. - use std::sync::atomic::Ordering; use std::sync::OnceLock; + use std::sync::atomic::Ordering; + use opentelemetry::KeyValue; use opentelemetry::global; use opentelemetry::metrics::Meter; use opentelemetry::metrics::MeterProvider as _; - use opentelemetry::KeyValue; use super::{ METRIC_GRAPH_TENANT_COUNT, METRIC_GRAPH_TUPLE_COUNT, METRIC_REVISION_CURRENT, @@ -203,20 +204,14 @@ pub mod otel_metrics { .u64_observable_gauge("aegis.graph.tuple_count") .with_description("Number of tuples in storage") .with_callback(|observer| { - observer.observe( - METRIC_GRAPH_TUPLE_COUNT.load(Ordering::Relaxed), - &[], - ); + observer.observe(METRIC_GRAPH_TUPLE_COUNT.load(Ordering::Relaxed), &[]); }) .build(); let _ = m .u64_observable_gauge("aegis.graph.tenant_count") .with_description("Number of tenants/namespaces") .with_callback(|observer| { - observer.observe( - METRIC_GRAPH_TENANT_COUNT.load(Ordering::Relaxed), - &[], - ); + observer.observe(METRIC_GRAPH_TENANT_COUNT.load(Ordering::Relaxed), &[]); }) .build(); let _ = m @@ -233,20 +228,14 @@ pub mod otel_metrics { .u64_observable_gauge("aegis.schema.version") .with_description("Current schema version") .with_callback(|observer| { - observer.observe( - METRIC_SCHEMA_VERSION.load(Ordering::Relaxed), - &[], - ); + observer.observe(METRIC_SCHEMA_VERSION.load(Ordering::Relaxed), &[]); }) .build(); let _ = m .u64_observable_gauge("aegis.revision.current") .with_description("Current revision number") .with_callback(|observer| { - observer.observe( - METRIC_REVISION_CURRENT.load(Ordering::Relaxed), - &[], - ); + observer.observe(METRIC_REVISION_CURRENT.load(Ordering::Relaxed), &[]); }) .build(); }); @@ -259,7 +248,13 @@ pub mod otel_metrics { .u64_counter("aegis.check.total") .with_description("Total number of authorization checks") .build(); - counter.add(1, &[KeyValue::new("allowed", if allowed { "true" } else { "false" })]); + counter.add( + 1, + &[KeyValue::new( + "allowed", + if allowed { "true" } else { "false" }, + )], + ); } /// Counter: checks that resulted in allow. @@ -300,7 +295,13 @@ pub mod otel_metrics { .with_description("Duration of authorization checks in milliseconds") .with_unit("ms") .build(); - histogram.record(duration_ms, &[KeyValue::new("allowed", if allowed { "true" } else { "false" })]); + histogram.record( + duration_ms, + &[KeyValue::new( + "allowed", + if allowed { "true" } else { "false" }, + )], + ); } /// Gauge (up-down counter): current cache size. @@ -325,23 +326,18 @@ pub mod otel_metrics { } } -#[cfg(feature = "telemetry")] -#[cfg(test)] +#[cfg(all(test, feature = "telemetry", feature = "sqlite"))] mod tests { use super::otel_metrics; use crate::engine::GraphEngine; - use crate::storage::sqlite::{SqliteConfig, SqliteStorage}; use crate::storage::StorageBackend; - use crate::types::{ - Relation, RelationshipTuple, ResourceId, Schema, SubjectId, - }; + use crate::storage::sqlite::{SqliteConfig, SqliteStorage}; + use crate::types::{Relation, RelationshipTuple, ResourceId, Schema, SubjectId}; use opentelemetry_sdk::metrics::InMemoryMetricExporter; use opentelemetry_sdk::metrics::PeriodicReader; use opentelemetry_sdk::metrics::SdkMeterProvider; - fn make_engine_with_provider( - provider: SdkMeterProvider, - ) -> GraphEngine { + fn make_engine_with_provider(provider: SdkMeterProvider) -> GraphEngine { let schema = Schema { schema_version: 1, namespace: "test".to_string(), @@ -399,9 +395,7 @@ mod tests { fn test_in_memory_metrics_exporter() { let exporter = InMemoryMetricExporter::default(); let reader = PeriodicReader::builder(exporter.clone()).build(); - let provider = SdkMeterProvider::builder() - .with_reader(reader) - .build(); + let provider = SdkMeterProvider::builder().with_reader(reader).build(); let engine = make_engine_with_provider(provider); @@ -417,9 +411,7 @@ mod tests { )) .unwrap(); - let _ = engine - .check(&subject, "read", &resource, None) - .unwrap(); + let _ = engine.check(&subject, "read", &resource, None).unwrap(); // Verify statics were updated assert!( diff --git a/crates/aegis-core/src/testing/fixtures.rs b/crates/aegis-core/src/testing/fixtures.rs index 8950382..d66d4cf 100644 --- a/crates/aegis-core/src/testing/fixtures.rs +++ b/crates/aegis-core/src/testing/fixtures.rs @@ -28,9 +28,9 @@ pub fn load_fixture_yaml(yaml: &str) -> AegisResult { let mut tuples = Vec::with_capacity(raw.tuples.len()); for rt in raw.tuples { - let subject = SubjectId::new(&rt.subject).map_err(|e| AegisError::Validation(e))?; - let relation = Relation::new(&rt.relation).map_err(|e| AegisError::Validation(e))?; - let object = ResourceId::new(&rt.object).map_err(|e| AegisError::Validation(e))?; + let subject = SubjectId::new(&rt.subject).map_err(AegisError::Validation)?; + let relation = Relation::new(&rt.relation).map_err(AegisError::Validation)?; + let object = ResourceId::new(&rt.object).map_err(AegisError::Validation)?; tuples.push((subject, relation, object)); } diff --git a/crates/aegis-core/src/types/analysis.rs b/crates/aegis-core/src/types/analysis.rs index aa8af6c..1c873f9 100644 --- a/crates/aegis-core/src/types/analysis.rs +++ b/crates/aegis-core/src/types/analysis.rs @@ -1,5 +1,5 @@ -use serde::{Deserialize, Serialize}; use crate::types::*; +use serde::{Deserialize, Serialize}; /// Reason a permission check was denied. #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/crates/aegis-core/src/types/identity.rs b/crates/aegis-core/src/types/identity.rs index 367c5c6..6f679b3 100644 --- a/crates/aegis-core/src/types/identity.rs +++ b/crates/aegis-core/src/types/identity.rs @@ -66,8 +66,6 @@ impl SubjectId { } } - - impl fmt::Display for SubjectId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) @@ -234,7 +232,12 @@ impl SubjectSet { partition_id: &PartitionId, consistency: &crate::types::ConsistencyMode, ) -> Result, crate::error::AegisError> { - let tuples = storage.list_by_object(partition_id, &self.object, Some(&self.relation), consistency)?; + let tuples = storage.list_by_object( + partition_id, + &self.object, + Some(&self.relation), + consistency, + )?; Ok(tuples.into_iter().map(|t| t.subject).collect()) } } diff --git a/crates/aegis-core/src/types/revision.rs b/crates/aegis-core/src/types/revision.rs index cf8d99a..7b99695 100644 --- a/crates/aegis-core/src/types/revision.rs +++ b/crates/aegis-core/src/types/revision.rs @@ -55,10 +55,11 @@ impl RevisionToken { } /// Controls the consistency guarantee for a read operation. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] pub enum ConsistencyMode { /// Fast path: read from the latest available local snapshot. /// May be slightly stale in multi-instance deployments. + #[default] MinimizeLatency, /// Read from a snapshot at least as fresh as the given revision. @@ -70,12 +71,6 @@ pub enum ConsistencyMode { FullyConsistent, } -impl Default for ConsistencyMode { - fn default() -> Self { - Self::MinimizeLatency - } -} - /// Represents the result of a graph mutation. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WriteResult { @@ -157,18 +152,13 @@ pub struct AccessReviewEntry { } /// Configuration for fail-closed vs fail-open behavior. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] pub enum FailClosedMode { + #[default] DenyOnError, AllowOnError, } -impl Default for FailClosedMode { - fn default() -> Self { - Self::DenyOnError - } -} - /// Represents a single audit log entry. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AuditEntry { diff --git a/crates/aegis-core/src/types/schema.rs b/crates/aegis-core/src/types/schema.rs index b18d33d..a1c527a 100644 --- a/crates/aegis-core/src/types/schema.rs +++ b/crates/aegis-core/src/types/schema.rs @@ -28,18 +28,13 @@ pub struct RelationDef { } /// The effect of a permission or deny rule. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] pub enum Effect { + #[default] Allow, Deny, } -impl Default for Effect { - fn default() -> Self { - Self::Allow - } -} - /// Defines a computed permission from a set of relations. #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct PermissionDef { @@ -357,7 +352,7 @@ mod tests { #[test] fn schema_missing_type_returns_none() { let s = sample_schema(); - assert!(s.has_relation("nonexistent", "owner") == false); + assert!(!s.has_relation("nonexistent", "owner")); assert!(s.relations_for_permission("nonexistent", "read").is_none()); } } diff --git a/crates/aegis-core/src/types/tuple.rs b/crates/aegis-core/src/types/tuple.rs index 1a0f3d9..e9c1c13 100644 --- a/crates/aegis-core/src/types/tuple.rs +++ b/crates/aegis-core/src/types/tuple.rs @@ -145,7 +145,11 @@ const MAX_TUPLE_SERIALIZED_SIZE: usize = 65_536; // 64 KiB /// Validate that a tuple's subject, relation, and object are all well-formed. /// Returns `Ok(())` if all three pass validation, or the first `ValidationError` encountered. -pub fn validate_tuple(subject: &str, relation: &str, object: &str) -> Result<(), crate::types::ValidationError> { +pub fn validate_tuple( + subject: &str, + relation: &str, + object: &str, +) -> Result<(), crate::types::ValidationError> { SubjectId::new(subject).map(|_| ())?; Relation::new(relation).map(|_| ())?; ResourceId::new(object).map(|_| ())?; diff --git a/crates/aegis-core/tests/soak.rs b/crates/aegis-core/tests/soak.rs index 3dab2a6..b64d187 100644 --- a/crates/aegis-core/tests/soak.rs +++ b/crates/aegis-core/tests/soak.rs @@ -1,9 +1,9 @@ #![cfg(feature = "sqlite")] use aegis_core::engine::GraphEngine; use aegis_core::schema::parse_schema; -use aegis_core::storage::sqlite::{SqliteConfig, SqliteStorage}; use aegis_core::storage::StorageBackend; use aegis_core::storage::TupleFilter; +use aegis_core::storage::sqlite::{SqliteConfig, SqliteStorage}; use aegis_core::types::*; use std::time::Instant; @@ -36,8 +36,8 @@ fn test_soak_no_memory_leak() { let start = Instant::now(); for i in 0..iterations { - let subject = SubjectId::new(&format!("user:soak{}", i)).unwrap(); - let resource = ResourceId::new(&format!("repo:soak{}", i)).unwrap(); + let subject = SubjectId::new(format!("user:soak{}", i)).unwrap(); + let resource = ResourceId::new(format!("repo:soak{}", i)).unwrap(); // Write engine @@ -99,8 +99,8 @@ fn test_throughput_target() { // Pre-seed tuples for i in 0..100 { - let subject = SubjectId::new(&format!("user:t{}", i)).unwrap(); - let resource = ResourceId::new(&format!("repo:t{}", i)).unwrap(); + let subject = SubjectId::new(format!("user:t{}", i)).unwrap(); + let resource = ResourceId::new(format!("repo:t{}", i)).unwrap(); engine .write(&RelationshipTuple::new( subject, diff --git a/crates/aegis-core/tests/stress.rs b/crates/aegis-core/tests/stress.rs index 5d14127..d578458 100644 --- a/crates/aegis-core/tests/stress.rs +++ b/crates/aegis-core/tests/stress.rs @@ -1,11 +1,11 @@ #![cfg(feature = "sqlite")] use aegis_core::engine::GraphEngine; use aegis_core::schema::parse_schema; -use aegis_core::storage::sqlite::{SqliteConfig, SqliteStorage}; use aegis_core::storage::StorageBackend; +use aegis_core::storage::sqlite::{SqliteConfig, SqliteStorage}; use aegis_core::types::*; -use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Instant; fn make_schema() -> Schema { @@ -31,11 +31,10 @@ types: /// Create an engine backed by a unique temp file (avoids :memory: isolation quirks /// with WAL + pooled connections under concurrent write load). fn make_file_engine(max_readers: u32) -> (GraphEngine, String) { - let path = format!( - "{}\\aegis_stress_{}.db", - std::env::temp_dir().display(), - fastrand::u64(..) - ); + let path = std::env::temp_dir() + .join(format!("aegis_stress_{}.db", fastrand::u64(..))) + .to_string_lossy() + .into_owned(); let config = SqliteConfig { path: path.clone(), max_readers, @@ -112,7 +111,7 @@ fn str004_read_during_write() { // Writer: add many viewer tuples to the same resource for i in 0..50 { - let viewer = SubjectId::new(&format!("user:viewer{}", i)).unwrap(); + let viewer = SubjectId::new(format!("user:viewer{}", i)).unwrap(); engine .write(&RelationshipTuple::new( viewer, @@ -129,7 +128,7 @@ fn str004_read_during_write() { // Verify all viewer writes persisted for i in 0..50 { - let viewer = SubjectId::new(&format!("user:viewer{}", i)).unwrap(); + let viewer = SubjectId::new(format!("user:viewer{}", i)).unwrap(); let result = engine.check(&viewer, "read", &resource, None).unwrap(); assert!(result.allowed, "viewer{} should have access after write", i); } @@ -150,8 +149,8 @@ fn str006_write_queue_depth() { for i in 0..100 { let engine = Arc::clone(&engine); handles.push(std::thread::spawn(move || { - let subject = SubjectId::new(&format!("user:sw{}", i)).unwrap(); - let resource = ResourceId::new(&format!("repo:sw{}", i)).unwrap(); + let subject = SubjectId::new(format!("user:sw{}", i)).unwrap(); + let resource = ResourceId::new(format!("repo:sw{}", i)).unwrap(); engine.write(&RelationshipTuple::new( subject, Relation::new("owner").unwrap(), @@ -161,13 +160,14 @@ fn str006_write_queue_depth() { } for h in handles { - h.join() - .unwrap() - .expect("concurrent write should succeed"); + h.join().unwrap().expect("concurrent write should succeed"); } // Verify revision increased - let rev = engine.storage().current_revision(&PartitionId::default()).unwrap(); + let rev = engine + .storage() + .current_revision(&PartitionId::default()) + .unwrap(); assert!( rev.as_u64() >= 100, "expected >= 100 writes, got rev {}", @@ -176,8 +176,8 @@ fn str006_write_queue_depth() { // Verify sample of tuples are queryable for i in 0..10 { - let subject = SubjectId::new(&format!("user:sw{}", i)).unwrap(); - let resource = ResourceId::new(&format!("repo:sw{}", i)).unwrap(); + let subject = SubjectId::new(format!("user:sw{}", i)).unwrap(); + let resource = ResourceId::new(format!("repo:sw{}", i)).unwrap(); let result = engine.check(&subject, "read", &resource, None).unwrap(); assert!(result.allowed, "tuple {} should exist", i); } @@ -197,10 +197,10 @@ fn str007_large_graph_stress() { // Create teams for t in 0..num_teams { - let team = ResourceId::new(&format!("team:t{}", t)).unwrap(); + let team = ResourceId::new(format!("team:t{}", t)).unwrap(); for m in 0..20 { let user_idx = (t * 20 + m) % num_subjects; - let user = SubjectId::new(&format!("user:u{}", user_idx)).unwrap(); + let user = SubjectId::new(format!("user:u{}", user_idx)).unwrap(); engine .write(&RelationshipTuple::new( user, @@ -214,8 +214,8 @@ fn str007_large_graph_stress() { // Create repos owned by teams let num_repos = 500; for r in 0..num_repos { - let team = SubjectId::new(&format!("team:t{}", r % num_teams)).unwrap(); - let repo = ResourceId::new(&format!("repo:r{}", r)).unwrap(); + let team = SubjectId::new(format!("team:t{}", r % num_teams)).unwrap(); + let repo = ResourceId::new(format!("repo:r{}", r)).unwrap(); engine .write(&RelationshipTuple::new( team, @@ -231,9 +231,8 @@ fn str007_large_graph_stress() { let mut latencies = Vec::with_capacity(num_checks); for _ in 0..num_checks { - let user = - SubjectId::new(&format!("user:u{}", fastrand::usize(0..num_subjects))).unwrap(); - let repo = ResourceId::new(&format!("repo:r{}", fastrand::usize(0..num_repos))).unwrap(); + let user = SubjectId::new(format!("user:u{}", fastrand::usize(0..num_subjects))).unwrap(); + let repo = ResourceId::new(format!("repo:r{}", fastrand::usize(0..num_repos))).unwrap(); let check_start = Instant::now(); let result = engine.check(&user, "access", &repo, None).unwrap(); latencies.push(check_start.elapsed()); @@ -272,8 +271,8 @@ fn str010_extended_soak() { let start = Instant::now(); for i in 0..iterations { - let subject = SubjectId::new(&format!("user:soak{}", i)).unwrap(); - let resource = ResourceId::new(&format!("repo:soak{}", i)).unwrap(); + let subject = SubjectId::new(format!("user:soak{}", i)).unwrap(); + let resource = ResourceId::new(format!("repo:soak{}", i)).unwrap(); engine .write(&RelationshipTuple::new( diff --git a/crates/aegis-core/tests/v1_closure.rs b/crates/aegis-core/tests/v1_closure.rs index 396a739..e742836 100644 --- a/crates/aegis-core/tests/v1_closure.rs +++ b/crates/aegis-core/tests/v1_closure.rs @@ -103,13 +103,11 @@ fn v1_m1_write_batch_validates_schema() { let result = engine.write_batch(&tuples); assert!(result.is_ok()); - let bad_tuples = vec![ - RelationshipTuple::new( - SubjectId::new("user:alice").unwrap(), - Relation::new("nonexistent").unwrap(), - ResourceId::new("repo:a").unwrap(), - ), - ]; + let bad_tuples = vec![RelationshipTuple::new( + SubjectId::new("user:alice").unwrap(), + Relation::new("nonexistent").unwrap(), + ResourceId::new("repo:a").unwrap(), + )]; let result = engine.write_batch(&bad_tuples); assert!(result.is_err()); assert!(matches!( @@ -202,11 +200,14 @@ fn v1_m3_transactions() { let mut txn = engine.transaction().unwrap(); - txn.write(&PartitionId::default(), &RelationshipTuple::new( - SubjectId::new("user:alice").unwrap(), - Relation::new("owner").unwrap(), - ResourceId::new("repo:txn-test").unwrap(), - )) + txn.write( + &PartitionId::default(), + &RelationshipTuple::new( + SubjectId::new("user:alice").unwrap(), + Relation::new("owner").unwrap(), + ResourceId::new("repo:txn-test").unwrap(), + ), + ) .unwrap(); let rev = txn.commit().unwrap(); @@ -223,13 +224,15 @@ fn v1_m3_transactions() { assert!(result.allowed); let mut txn2 = engine.transaction().unwrap(); - txn2 - .write(&PartitionId::default(), &RelationshipTuple::new( + txn2.write( + &PartitionId::default(), + &RelationshipTuple::new( SubjectId::new("user:bob").unwrap(), Relation::new("owner").unwrap(), ResourceId::new("repo:txn-test").unwrap(), - )) - .unwrap(); + ), + ) + .unwrap(); txn2.rollback().unwrap(); let result = engine @@ -277,7 +280,10 @@ fn v1_m3_backup_restore_roundtrip() { .tuples; assert_eq!(all_tuples.len(), 2); - let rev_before = engine.storage().current_revision(&PartitionId::default()).unwrap(); + let rev_before = engine + .storage() + .current_revision(&PartitionId::default()) + .unwrap(); let recovered = engine.recover_from_events(None).unwrap(); assert!(recovered.as_u64() >= rev_before.as_u64()); diff --git a/crates/aegis-core/tests/v2_multi_model.rs b/crates/aegis-core/tests/v2_multi_model.rs index cdbebf9..cce0737 100644 --- a/crates/aegis-core/tests/v2_multi_model.rs +++ b/crates/aegis-core/tests/v2_multi_model.rs @@ -1,12 +1,10 @@ #![cfg(feature = "sqlite")] use aegis_core::engine::GraphEngine; -use aegis_core::engine::{acl, condition, rbac}; use aegis_core::engine::condition::ConditionEvalContext; +use aegis_core::engine::{acl, condition, rbac}; use aegis_core::schema::parse_schema; use aegis_core::storage::sqlite::{SqliteConfig, SqliteStorage}; -use aegis_core::types::{ - PartitionId, Relation, RelationshipTuple, ResourceId, Schema, SubjectId, -}; +use aegis_core::types::{PartitionId, Relation, RelationshipTuple, ResourceId, Schema, SubjectId}; use std::collections::HashMap; fn make_schema_v2() -> Schema { @@ -100,7 +98,8 @@ fn v2_m1_rbac_assign_check_unassign() { let token = rbac::assign_role(&engine, &alice, "owner", &repo).unwrap(); assert!(token.revision.as_u64() > 0); - let result = rbac::check_role(&engine, &PartitionId::default(), &alice, "owner", &repo).unwrap(); + let result = + rbac::check_role(&engine, &PartitionId::default(), &alice, "owner", &repo).unwrap(); assert!(result.allowed); let roles = rbac::get_roles(&engine, &alice, &repo).unwrap(); @@ -108,7 +107,8 @@ fn v2_m1_rbac_assign_check_unassign() { assert!(roles.contains(&"owner".to_string())); rbac::unassign_role(&engine, &alice, "owner", &repo).unwrap(); - let result = rbac::check_role(&engine, &PartitionId::default(), &alice, "owner", &repo).unwrap(); + let result = + rbac::check_role(&engine, &PartitionId::default(), &alice, "owner", &repo).unwrap(); assert!(!result.allowed); let roles = rbac::get_roles(&engine, &alice, &repo).unwrap(); @@ -154,7 +154,8 @@ fn v2_m1_rbac_role_does_not_imply_different_resource() { rbac::assign_role(&engine, &alice, "owner", &repo_a).unwrap(); - let result = rbac::check_role(&engine, &PartitionId::default(), &alice, "owner", &repo_b).unwrap(); + let result = + rbac::check_role(&engine, &PartitionId::default(), &alice, "owner", &repo_b).unwrap(); assert!(!result.allowed); } @@ -192,9 +193,7 @@ fn v2_m2_acl_grant_resolves_permission_to_relation() { acl::grant(&engine, &alice, "read", &repo).unwrap(); - let tuples = engine - .list_by_object(&repo, None, None) - .unwrap(); + let tuples = engine.list_by_object(&repo, None, None).unwrap(); assert_eq!(tuples.len(), 1); assert_eq!(tuples[0].relation.as_str(), "viewer"); } @@ -316,7 +315,10 @@ fn v2_m4_tuple_future_expiry_still_works() { engine.write(&tuple).unwrap(); let result = engine.check(&alice, "read", &repo, None).unwrap(); - assert!(result.allowed, "future-expiry tuple should still grant access"); + assert!( + result.allowed, + "future-expiry tuple should still grant access" + ); } #[test] @@ -336,7 +338,10 @@ fn v2_m5_abac_condition_with_context_integration() { let result = engine .check_with_context(&alice, "view", &doc, None, ctx) .unwrap(); - assert!(result.allowed, "matching context (role eq admin) should allow"); + assert!( + result.allowed, + "matching context (role eq admin) should allow" + ); let mut ctx = condition::ConditionEvalContext::default(); ctx.resource_meta @@ -380,15 +385,23 @@ types: rbac::assign_role(&engine, &alice, "reader", &doc).unwrap(); let mut ctx = condition::ConditionEvalContext::default(); - ctx.subject_meta.insert("role".to_string(), "admin".to_string()); - ctx.resource_meta.insert("region".to_string(), "us-east".to_string()); - let result = engine.check_with_context(&alice, "view", &doc, None, ctx).unwrap(); + ctx.subject_meta + .insert("role".to_string(), "admin".to_string()); + ctx.resource_meta + .insert("region".to_string(), "us-east".to_string()); + let result = engine + .check_with_context(&alice, "view", &doc, None, ctx) + .unwrap(); assert!(result.allowed, "AND: both match should allow"); let mut ctx = condition::ConditionEvalContext::default(); - ctx.subject_meta.insert("role".to_string(), "admin".to_string()); - ctx.resource_meta.insert("region".to_string(), "eu-west".to_string()); - let result = engine.check_with_context(&alice, "view", &doc, None, ctx).unwrap(); + ctx.subject_meta + .insert("role".to_string(), "admin".to_string()); + ctx.resource_meta + .insert("region".to_string(), "eu-west".to_string()); + let result = engine + .check_with_context(&alice, "view", &doc, None, ctx) + .unwrap(); assert!(!result.allowed, "AND: one fails should deny"); } @@ -414,13 +427,19 @@ types: rbac::assign_role(&engine, &alice, "reader", &doc).unwrap(); let mut ctx = condition::ConditionEvalContext::default(); - ctx.resource_meta.insert("region".to_string(), "us-east".to_string()); - let result = engine.check_with_context(&alice, "view", &doc, None, ctx).unwrap(); + ctx.resource_meta + .insert("region".to_string(), "us-east".to_string()); + let result = engine + .check_with_context(&alice, "view", &doc, None, ctx) + .unwrap(); assert!(result.allowed, "NOT restricted should allow us-east"); let mut ctx = condition::ConditionEvalContext::default(); - ctx.resource_meta.insert("region".to_string(), "restricted".to_string()); - let result = engine.check_with_context(&alice, "view", &doc, None, ctx).unwrap(); + ctx.resource_meta + .insert("region".to_string(), "restricted".to_string()); + let result = engine + .check_with_context(&alice, "view", &doc, None, ctx) + .unwrap(); assert!(!result.allowed, "NOT restricted should deny restricted"); } @@ -436,7 +455,10 @@ fn v2_m6_effect_deny_on_permission() { assert!(view.allowed, "view permission should still allow"); let blocked = engine.check(&alice, "blocked", &secret, None).unwrap(); - assert!(!blocked.allowed, "blocked permission with Deny effect should deny"); + assert!( + !blocked.allowed, + "blocked permission with Deny effect should deny" + ); } #[test] @@ -544,9 +566,7 @@ fn v2_m9_explain_shows_deny_path() { rbac::assign_role(&engine, &alice, "owner", &repo).unwrap(); rbac::assign_role(&engine, &alice, "banned", &repo).unwrap(); - let explain = engine - .explain(&alice, "read", &repo, None) - .unwrap(); + let explain = engine.explain(&alice, "read", &repo, None).unwrap(); assert!(!explain.allowed); } @@ -572,7 +592,8 @@ fn v2_m10_abac_dry_run_with_context() { rbac::assign_role(&engine, &alice, "reader", &doc).unwrap(); let mut ctx = condition::ConditionEvalContext::default(); - ctx.subject_meta.insert("role".to_string(), "admin".to_string()); + ctx.subject_meta + .insert("role".to_string(), "admin".to_string()); let result = engine .check_dry_run_with_context(&alice, "view", &doc, None, ctx) .unwrap(); @@ -580,7 +601,6 @@ fn v2_m10_abac_dry_run_with_context() { } /// ── V2.5 Role hierarchy ────────────────────────────────────────────────────── - fn make_schema_role_hierarchy() -> Schema { let yaml = r#" schemaVersion: 2 @@ -680,9 +700,18 @@ fn v2_5_role_hierarchy_get_roles_includes_inherited() { rbac::assign_role(&engine, &dave, "admin", &repo).unwrap(); let roles = rbac::get_roles(&engine, &dave, &repo).unwrap(); - assert!(roles.contains(&"admin".to_string()), "should have admin role"); - assert!(roles.contains(&"editor".to_string()), "should have editor role (inherited)"); - assert!(roles.contains(&"viewer".to_string()), "should have viewer role (inherited)"); + assert!( + roles.contains(&"admin".to_string()), + "should have admin role" + ); + assert!( + roles.contains(&"editor".to_string()), + "should have editor role (inherited)" + ); + assert!( + roles.contains(&"viewer".to_string()), + "should have viewer role (inherited)" + ); } #[test] @@ -697,7 +726,10 @@ fn v2_5_role_hierarchy_check_role_resolves_inheritance() { // check_role for "viewer" should return true (editor inherits from viewer, // so editor IS considered a viewer too) let r = rbac::check_role(&engine, &PartitionId::default(), &eve, "viewer", &repo).unwrap(); - assert!(r.allowed, "editor should be recognized as having viewer role via inheritance"); + assert!( + r.allowed, + "editor should be recognized as having viewer role via inheritance" + ); // check_role for "admin" should return false (editor does NOT inherit from admin) let r = rbac::check_role(&engine, &PartitionId::default(), &eve, "admin", &repo).unwrap(); @@ -705,7 +737,6 @@ fn v2_5_role_hierarchy_check_role_resolves_inheritance() { } /// ── V2.5 Subject-set resolution ────────────────────────────────────────────── - fn make_schema_subject_set() -> Schema { let yaml = r#" schemaVersion: 2 @@ -752,12 +783,18 @@ fn v2_5_subject_set_direct_resolution() { // user:alice should be able to edit repo:fluxbus via subject-set resolution let result = engine.check(&alice, "edit", &repo, None).unwrap(); - assert!(result.allowed, "alice should inherit editor via subject-set membership"); + assert!( + result.allowed, + "alice should inherit editor via subject-set membership" + ); // A non-member should NOT get access let bob = SubjectId::new("user:bob").unwrap(); let result2 = engine.check(&bob, "edit", &repo, None).unwrap(); - assert!(!result2.allowed, "bob should not have editor (not a member of team:eng)"); + assert!( + !result2.allowed, + "bob should not have editor (not a member of team:eng)" + ); } #[test] @@ -799,7 +836,10 @@ fn v2_5_subject_set_non_member_denied() { // alice is NOT a member of team:eng, so should be denied let result = engine.check(&alice, "edit", &repo, None).unwrap(); - assert!(!result.allowed, "alice is in team:sre, not team:eng — should be denied"); + assert!( + !result.allowed, + "alice is in team:sre, not team:eng — should be denied" + ); } #[test] @@ -819,7 +859,10 @@ fn v2_5_conditional_tuple_denied_without_context() { // Without context, condition cannot be evaluated → tuple is skipped let result = engine.check(&alice, "read", &repo, None).unwrap(); - assert!(!result.allowed, "conditional tuple should be denied without context"); + assert!( + !result.allowed, + "conditional tuple should be denied without context" + ); } #[test] @@ -844,8 +887,13 @@ fn v2_5_conditional_tuple_allowed_with_matching_context() { ..Default::default() }; - let result = engine.check_with_context(&alice, "read", &repo, None, ctx).unwrap(); - assert!(result.allowed, "conditional tuple should be allowed with matching context"); + let result = engine + .check_with_context(&alice, "read", &repo, None, ctx) + .unwrap(); + assert!( + result.allowed, + "conditional tuple should be allowed with matching context" + ); } #[test] @@ -870,8 +918,13 @@ fn v2_5_conditional_tuple_denied_with_non_matching_context() { ..Default::default() }; - let result = engine.check_with_context(&alice, "read", &repo, None, ctx).unwrap(); - assert!(!result.allowed, "conditional tuple should be denied with non-matching context"); + let result = engine + .check_with_context(&alice, "read", &repo, None, ctx) + .unwrap(); + assert!( + !result.allowed, + "conditional tuple should be denied with non-matching context" + ); } #[test] @@ -890,7 +943,10 @@ fn v2_5_conditional_tuple_unconditional_tuples_still_work() { .unwrap(); let result = engine.check(&alice, "read", &repo, None).unwrap(); - assert!(result.allowed, "unconditional tuple should still work without context"); + assert!( + result.allowed, + "unconditional tuple should still work without context" + ); } #[test] @@ -922,8 +978,11 @@ fn v2_5_conditional_tuple_with_expiry() { ..Default::default() }; - let result = engine.check_with_context(&alice, "read", &repo, None, ctx).unwrap(); - assert!(!result.allowed, "expired conditional tuple should be denied even with matching context"); + let result = engine + .check_with_context(&alice, "read", &repo, None, ctx) + .unwrap(); + assert!( + !result.allowed, + "expired conditional tuple should be denied even with matching context" + ); } - - diff --git a/crates/aegis-ffi/src/lib.rs b/crates/aegis-ffi/src/lib.rs index 6dba05a..8263c2f 100644 --- a/crates/aegis-ffi/src/lib.rs +++ b/crates/aegis-ffi/src/lib.rs @@ -1,12 +1,14 @@ +#![allow(clippy::not_unsafe_ptr_arg_deref)] + use std::ffi::{CStr, CString}; use std::panic::{self, AssertUnwindSafe}; -use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Mutex; +use std::sync::atomic::{AtomicBool, Ordering}; +use aegis_core::engine::GraphEngine; use aegis_core::engine::hooks; use aegis_core::engine::ratelimit::{RateLimitConfig, TokenBucketRateLimiter}; use aegis_core::engine::watch::{WatchEventType, WatchFilter, WatchSubscription}; -use aegis_core::engine::GraphEngine; use aegis_core::schema::parse_schema; use aegis_core::storage::sqlite::{SqliteConfig, SqliteStorage}; use aegis_core::storage::{StorageBackend, StorageTransaction}; @@ -42,7 +44,11 @@ pub struct AegisHealthResult { // ── Engine lifecycle ── -fn create_engine_base(path: &str, yaml: &str, config_json: *const libc::c_char) -> Result<*mut AegisEngine, String> { +fn create_engine_base( + path: &str, + yaml: &str, + config_json: *const libc::c_char, +) -> Result<*mut AegisEngine, String> { let mut cfg = SqliteConfig { path: path.to_string(), max_readers: 4, @@ -63,10 +69,18 @@ fn create_engine_base(path: &str, yaml: &str, config_json: *const libc::c_char) mmap_size: Option, } if let Ok(overrides) = serde_json::from_str::(json_str) { - if let Some(v) = overrides.max_readers { cfg.max_readers = v; } - if let Some(v) = overrides.busy_timeout_ms { cfg.busy_timeout_ms = v; } - if let Some(v) = overrides.wal_mode { cfg.wal_mode = v; } - if let Some(v) = overrides.mmap_size { cfg.mmap_size = v; } + if let Some(v) = overrides.max_readers { + cfg.max_readers = v; + } + if let Some(v) = overrides.busy_timeout_ms { + cfg.busy_timeout_ms = v; + } + if let Some(v) = overrides.wal_mode { + cfg.wal_mode = v; + } + if let Some(v) = overrides.mmap_size { + cfg.mmap_size = v; + } } } @@ -163,30 +177,32 @@ pub extern "C" fn aegis_engine_check( allowed: false, revision: 0, error: err, - } + }; } }; - match panic::catch_unwind(AssertUnwindSafe(|| -> Result { - let subject_str = c_str_to_str(subject)?; - let permission_str = c_str_to_str(permission)?; - let resource_str = c_str_to_str(resource)?; + match panic::catch_unwind(AssertUnwindSafe( + || -> Result { + let subject_str = c_str_to_str(subject)?; + let permission_str = c_str_to_str(permission)?; + let resource_str = c_str_to_str(resource)?; - let subject_id = - SubjectId::new(&subject_str).map_err(|e| error_string(&e.to_string()))?; - let resource_id = - ResourceId::new(&resource_str).map_err(|e| error_string(&e.to_string()))?; + let subject_id = + SubjectId::new(&subject_str).map_err(|e| error_string(&e.to_string()))?; + let resource_id = + ResourceId::new(&resource_str).map_err(|e| error_string(&e.to_string()))?; - let result = eng - .check(&subject_id, &permission_str, &resource_id, None) - .map_err(|e| error_string(&e.to_string()))?; + let result = eng + .check(&subject_id, &permission_str, &resource_id, None) + .map_err(|e| error_string(&e.to_string()))?; - Ok(AegisCheckResult { - allowed: result.allowed, - revision: result.revision.as_u64(), - error: std::ptr::null_mut(), - }) - })) { + Ok(AegisCheckResult { + allowed: result.allowed, + revision: result.revision.as_u64(), + error: std::ptr::null_mut(), + }) + }, + )) { Ok(Ok(res)) => res, Ok(Err(err)) => AegisCheckResult { allowed: false, @@ -221,33 +237,35 @@ pub extern "C" fn aegis_engine_write( return AegisWriteResult { revision: 0, error: err, - } + }; } }; - match panic::catch_unwind(AssertUnwindSafe(|| -> Result { - let subject_str = c_str_to_str(subject)?; - let relation_str = c_str_to_str(relation)?; - let resource_str = c_str_to_str(resource)?; + match panic::catch_unwind(AssertUnwindSafe( + || -> Result { + let subject_str = c_str_to_str(subject)?; + let relation_str = c_str_to_str(relation)?; + let resource_str = c_str_to_str(resource)?; - let subject_id = - SubjectId::new(&subject_str).map_err(|e| error_string(&e.to_string()))?; - let relation_id = - Relation::new(&relation_str).map_err(|e| error_string(&e.to_string()))?; - let resource_id = - ResourceId::new(&resource_str).map_err(|e| error_string(&e.to_string()))?; + let subject_id = + SubjectId::new(&subject_str).map_err(|e| error_string(&e.to_string()))?; + let relation_id = + Relation::new(&relation_str).map_err(|e| error_string(&e.to_string()))?; + let resource_id = + ResourceId::new(&resource_str).map_err(|e| error_string(&e.to_string()))?; - let tuple = RelationshipTuple::new(subject_id, relation_id, resource_id); + let tuple = RelationshipTuple::new(subject_id, relation_id, resource_id); - let revision_token = eng - .write(&tuple) - .map_err(|e| error_string(&e.to_string()))?; + let revision_token = eng + .write(&tuple) + .map_err(|e| error_string(&e.to_string()))?; - Ok(AegisWriteResult { - revision: revision_token.revision.as_u64(), - error: std::ptr::null_mut(), - }) - })) { + Ok(AegisWriteResult { + revision: revision_token.revision.as_u64(), + error: std::ptr::null_mut(), + }) + }, + )) { Ok(Ok(res)) => res, Ok(Err(err)) => AegisWriteResult { revision: 0, @@ -280,37 +298,37 @@ pub extern "C" fn aegis_engine_delete( return AegisWriteResult { revision: 0, error: err, - } + }; } }; - match panic::catch_unwind(AssertUnwindSafe(|| -> Result { - let subject_str = c_str_to_str(subject)?; - let relation_str = c_str_to_str(relation)?; - let resource_str = c_str_to_str(resource)?; + match panic::catch_unwind(AssertUnwindSafe( + || -> Result { + let subject_str = c_str_to_str(subject)?; + let relation_str = c_str_to_str(relation)?; + let resource_str = c_str_to_str(resource)?; - let subject_id = - SubjectId::new(&subject_str).map_err(|e| error_string(&e.to_string()))?; - let relation_id = - Relation::new(&relation_str).map_err(|e| error_string(&e.to_string()))?; - let resource_id = - ResourceId::new(&resource_str).map_err(|e| error_string(&e.to_string()))?; + let subject_id = + SubjectId::new(&subject_str).map_err(|e| error_string(&e.to_string()))?; + let relation_id = + Relation::new(&relation_str).map_err(|e| error_string(&e.to_string()))?; + let resource_id = + ResourceId::new(&resource_str).map_err(|e| error_string(&e.to_string()))?; - let key = TupleKey { - subject: subject_id, - relation: relation_id, - object: resource_id, - }; + let key = TupleKey { + subject: subject_id, + relation: relation_id, + object: resource_id, + }; - let revision_token = eng - .delete(&key) - .map_err(|e| error_string(&e.to_string()))?; + let revision_token = eng.delete(&key).map_err(|e| error_string(&e.to_string()))?; - Ok(AegisWriteResult { - revision: revision_token.revision.as_u64(), - error: std::ptr::null_mut(), - }) - })) { + Ok(AegisWriteResult { + revision: revision_token.revision.as_u64(), + error: std::ptr::null_mut(), + }) + }, + )) { Ok(Ok(res)) => res, Ok(Err(err)) => AegisWriteResult { revision: 0, @@ -331,9 +349,7 @@ pub extern "C" fn aegis_engine_delete( } #[unsafe(no_mangle)] -pub extern "C" fn aegis_engine_health( - engine: *mut AegisEngine, -) -> AegisHealthResult { +pub extern "C" fn aegis_engine_health(engine: *mut AegisEngine) -> AegisHealthResult { let eng = match engine_from_ptr(engine) { Ok(e) => e, Err(err) => { @@ -342,7 +358,7 @@ pub extern "C" fn aegis_engine_health( revision: 0, schema_version: 0, error: err, - } + }; } }; @@ -428,29 +444,34 @@ pub extern "C" fn aegis_engine_explain( resolved_via: std::ptr::null_mut(), duration_ms: 0, error: err, - } + }; } }; - match panic::catch_unwind(AssertUnwindSafe(|| -> Result { - let subject_str = c_str_to_str(subject)?; - let permission_str = c_str_to_str(permission)?; - let resource_str = c_str_to_str(resource)?; - let subject_id = SubjectId::new(&subject_str).map_err(|e| error_string(&e.to_string()))?; - let resource_id = ResourceId::new(&resource_str).map_err(|e| error_string(&e.to_string()))?; - let result = eng.explain(&subject_id, &permission_str, &resource_id, None) - .map_err(|e| error_string(&e.to_string()))?; - let trace_json = serde_json::to_string(&result.trace) - .map_err(|e| error_string(&e.to_string()))?; - Ok(AegisExplainResult { - allowed: result.allowed, - revision: result.revision.as_u64(), - trace_json: error_string(&trace_json), - resolved_via: error_string(&result.resolved_via), - duration_ms: result.duration_ms as u64, - error: std::ptr::null_mut(), - }) - })) { + match panic::catch_unwind(AssertUnwindSafe( + || -> Result { + let subject_str = c_str_to_str(subject)?; + let permission_str = c_str_to_str(permission)?; + let resource_str = c_str_to_str(resource)?; + let subject_id = + SubjectId::new(&subject_str).map_err(|e| error_string(&e.to_string()))?; + let resource_id = + ResourceId::new(&resource_str).map_err(|e| error_string(&e.to_string()))?; + let result = eng + .explain(&subject_id, &permission_str, &resource_id, None) + .map_err(|e| error_string(&e.to_string()))?; + let trace_json = + serde_json::to_string(&result.trace).map_err(|e| error_string(&e.to_string()))?; + Ok(AegisExplainResult { + allowed: result.allowed, + revision: result.revision.as_u64(), + trace_json: error_string(&trace_json), + resolved_via: error_string(&result.resolved_via), + duration_ms: result.duration_ms as u64, + error: std::ptr::null_mut(), + }) + }, + )) { Ok(Ok(res)) => res, Ok(Err(err)) => AegisExplainResult { allowed: false, @@ -461,7 +482,9 @@ pub extern "C" fn aegis_engine_explain( error: err, }, Err(panic) => { - let msg = panic.downcast_ref::<&str>().map(|s| s.to_string()) + let msg = panic + .downcast_ref::<&str>() + .map(|s| s.to_string()) .or_else(|| panic.downcast_ref::().cloned()) .unwrap_or_else(|| "unknown panic".to_string()); AegisExplainResult { @@ -486,35 +509,52 @@ pub extern "C" fn aegis_engine_list_by_object( ) -> AegisListResult { let eng = match engine_from_ptr(engine) { Ok(e) => e, - Err(err) => return AegisListResult { tuples_json: std::ptr::null_mut(), error: err }, - }; - - match panic::catch_unwind(AssertUnwindSafe(|| -> Result { - let object_str = c_str_to_str(object)?; - let object_id = ResourceId::new(&object_str).map_err(|e| error_string(&e.to_string()))?; - let rel = if relation.is_null() { - None - } else { - let rel_str = c_str_to_str(relation)?; - Some(Relation::new(&rel_str).map_err(|e| error_string(&e.to_string()))?) - }; - let tuples = eng.list_by_object(&object_id, rel.as_ref(), None) - .map_err(|e| error_string(&e.to_string()))?; - let json = serde_json::to_string(&tuples.iter().map(|t| { + Err(err) => { + return AegisListResult { + tuples_json: std::ptr::null_mut(), + error: err, + }; + } + }; + + match panic::catch_unwind(AssertUnwindSafe( + || -> Result { + let object_str = c_str_to_str(object)?; + let object_id = + ResourceId::new(&object_str).map_err(|e| error_string(&e.to_string()))?; + let rel = if relation.is_null() { + None + } else { + let rel_str = c_str_to_str(relation)?; + Some(Relation::new(&rel_str).map_err(|e| error_string(&e.to_string()))?) + }; + let tuples = eng + .list_by_object(&object_id, rel.as_ref(), None) + .map_err(|e| error_string(&e.to_string()))?; + let json = serde_json::to_string(&tuples.iter().map(|t| { serde_json::json!({"subject": t.subject.as_str(), "relation": t.relation.as_str(), "object": t.object.as_str()}) }).collect::>()).map_err(|e| error_string(&e.to_string()))?; - Ok(AegisListResult { - tuples_json: error_string(&json), - error: std::ptr::null_mut(), - }) - })) { + Ok(AegisListResult { + tuples_json: error_string(&json), + error: std::ptr::null_mut(), + }) + }, + )) { Ok(Ok(res)) => res, - Ok(Err(err)) => AegisListResult { tuples_json: std::ptr::null_mut(), error: err }, + Ok(Err(err)) => AegisListResult { + tuples_json: std::ptr::null_mut(), + error: err, + }, Err(panic) => { - let msg = panic.downcast_ref::<&str>().map(|s| s.to_string()) + let msg = panic + .downcast_ref::<&str>() + .map(|s| s.to_string()) .or_else(|| panic.downcast_ref::().cloned()) .unwrap_or_else(|| "unknown panic".to_string()); - AegisListResult { tuples_json: std::ptr::null_mut(), error: error_string(&msg) } + AegisListResult { + tuples_json: std::ptr::null_mut(), + error: error_string(&msg), + } } } } @@ -529,35 +569,52 @@ pub extern "C" fn aegis_engine_list_by_subject( ) -> AegisListResult { let eng = match engine_from_ptr(engine) { Ok(e) => e, - Err(err) => return AegisListResult { tuples_json: std::ptr::null_mut(), error: err }, - }; - - match panic::catch_unwind(AssertUnwindSafe(|| -> Result { - let subject_str = c_str_to_str(subject)?; - let subject_id = SubjectId::new(&subject_str).map_err(|e| error_string(&e.to_string()))?; - let rel = if relation.is_null() { - None - } else { - let rel_str = c_str_to_str(relation)?; - Some(Relation::new(&rel_str).map_err(|e| error_string(&e.to_string()))?) - }; - let tuples = eng.list_by_subject(&subject_id, rel.as_ref(), None) - .map_err(|e| error_string(&e.to_string()))?; - let json = serde_json::to_string(&tuples.iter().map(|t| { + Err(err) => { + return AegisListResult { + tuples_json: std::ptr::null_mut(), + error: err, + }; + } + }; + + match panic::catch_unwind(AssertUnwindSafe( + || -> Result { + let subject_str = c_str_to_str(subject)?; + let subject_id = + SubjectId::new(&subject_str).map_err(|e| error_string(&e.to_string()))?; + let rel = if relation.is_null() { + None + } else { + let rel_str = c_str_to_str(relation)?; + Some(Relation::new(&rel_str).map_err(|e| error_string(&e.to_string()))?) + }; + let tuples = eng + .list_by_subject(&subject_id, rel.as_ref(), None) + .map_err(|e| error_string(&e.to_string()))?; + let json = serde_json::to_string(&tuples.iter().map(|t| { serde_json::json!({"subject": t.subject.as_str(), "relation": t.relation.as_str(), "object": t.object.as_str()}) }).collect::>()).map_err(|e| error_string(&e.to_string()))?; - Ok(AegisListResult { - tuples_json: error_string(&json), - error: std::ptr::null_mut(), - }) - })) { + Ok(AegisListResult { + tuples_json: error_string(&json), + error: std::ptr::null_mut(), + }) + }, + )) { Ok(Ok(res)) => res, - Ok(Err(err)) => AegisListResult { tuples_json: std::ptr::null_mut(), error: err }, + Ok(Err(err)) => AegisListResult { + tuples_json: std::ptr::null_mut(), + error: err, + }, Err(panic) => { - let msg = panic.downcast_ref::<&str>().map(|s| s.to_string()) + let msg = panic + .downcast_ref::<&str>() + .map(|s| s.to_string()) .or_else(|| panic.downcast_ref::().cloned()) .unwrap_or_else(|| "unknown panic".to_string()); - AegisListResult { tuples_json: std::ptr::null_mut(), error: error_string(&msg) } + AegisListResult { + tuples_json: std::ptr::null_mut(), + error: error_string(&msg), + } } } } @@ -571,34 +628,60 @@ pub extern "C" fn aegis_engine_write_batch( ) -> AegisWriteResult { let eng = match engine_from_ptr(engine) { Ok(e) => e, - Err(err) => return AegisWriteResult { revision: 0, error: err }, - }; - - match panic::catch_unwind(AssertUnwindSafe(|| -> Result { - let json_str = c_str_to_str(tuples_json)?; - let raw: Vec = serde_json::from_str(&json_str) - .map_err(|e| error_string(&e.to_string()))?; - let mut tuples = Vec::new(); - for item in raw { - let subject = item["subject"].as_str().ok_or_else(|| error_string("missing subject"))?; - let relation = item["relation"].as_str().ok_or_else(|| error_string("missing relation"))?; - let object = item["object"].as_str().ok_or_else(|| error_string("missing object"))?; - tuples.push(RelationshipTuple::new( - SubjectId::new(subject).map_err(|e| error_string(&e.to_string()))?, - Relation::new(relation).map_err(|e| error_string(&e.to_string()))?, - ResourceId::new(object).map_err(|e| error_string(&e.to_string()))?, - )); - } - let rev = eng.write_batch(&tuples).map_err(|e| error_string(&e.to_string()))?; - Ok(AegisWriteResult { revision: rev.revision.as_u64(), error: std::ptr::null_mut() }) - })) { + Err(err) => { + return AegisWriteResult { + revision: 0, + error: err, + }; + } + }; + + match panic::catch_unwind(AssertUnwindSafe( + || -> Result { + let json_str = c_str_to_str(tuples_json)?; + let raw: Vec = + serde_json::from_str(&json_str).map_err(|e| error_string(&e.to_string()))?; + let mut tuples = Vec::new(); + for item in raw { + let subject = item["subject"] + .as_str() + .ok_or_else(|| error_string("missing subject"))?; + let relation = item["relation"] + .as_str() + .ok_or_else(|| error_string("missing relation"))?; + let object = item["object"] + .as_str() + .ok_or_else(|| error_string("missing object"))?; + tuples.push(RelationshipTuple::new( + SubjectId::new(subject).map_err(|e| error_string(&e.to_string()))?, + Relation::new(relation).map_err(|e| error_string(&e.to_string()))?, + ResourceId::new(object).map_err(|e| error_string(&e.to_string()))?, + )); + } + let rev = eng + .write_batch(&tuples) + .map_err(|e| error_string(&e.to_string()))?; + Ok(AegisWriteResult { + revision: rev.revision.as_u64(), + error: std::ptr::null_mut(), + }) + }, + )) { Ok(Ok(res)) => res, - Ok(Err(err)) => AegisWriteResult { revision: 0, error: err }, + Ok(Err(err)) => AegisWriteResult { + revision: 0, + error: err, + }, Err(panic) => { - let msg = panic.downcast_ref::<&str>().map(|s| s.to_string()) + let msg = panic + .downcast_ref::<&str>() + .map(|s| s.to_string()) .or_else(|| panic.downcast_ref::().cloned()) .unwrap_or_else(|| "unknown panic".to_string()); - AegisWriteResult { revision: 0, error: error_string(&msg) } + AegisWriteResult { + revision: 0, + error: error_string(&msg), + } } } } @@ -615,10 +698,13 @@ pub extern "C" fn aegis_engine_migrate( Err(err) => return err, }; - match panic::catch_unwind(AssertUnwindSafe(|| -> Result<*mut libc::c_char, *mut libc::c_char> { - eng.migrate(target_version as u32).map_err(|e| error_string(&e.to_string()))?; - Ok(std::ptr::null_mut()) - })) { + match panic::catch_unwind(AssertUnwindSafe( + || -> Result<*mut libc::c_char, *mut libc::c_char> { + eng.migrate(target_version as u32) + .map_err(|e| error_string(&e.to_string()))?; + Ok(std::ptr::null_mut()) + }, + )) { Ok(Ok(ptr)) => ptr, Ok(Err(err)) => err, Err(_) => error_string("panic during migration"), @@ -634,22 +720,43 @@ pub extern "C" fn aegis_engine_delete_object( ) -> AegisWriteResult { let eng = match engine_from_ptr(engine) { Ok(e) => e, - Err(err) => return AegisWriteResult { revision: 0, error: err }, + Err(err) => { + return AegisWriteResult { + revision: 0, + error: err, + }; + } }; - match panic::catch_unwind(AssertUnwindSafe(|| -> Result { - let object_str = c_str_to_str(object)?; - let object_id = ResourceId::new(&object_str).map_err(|e| error_string(&e.to_string()))?; - let rev = eng.delete_object(&object_id).map_err(|e| error_string(&e.to_string()))?; - Ok(AegisWriteResult { revision: rev.revision.as_u64(), error: std::ptr::null_mut() }) - })) { + match panic::catch_unwind(AssertUnwindSafe( + || -> Result { + let object_str = c_str_to_str(object)?; + let object_id = + ResourceId::new(&object_str).map_err(|e| error_string(&e.to_string()))?; + let rev = eng + .delete_object(&object_id) + .map_err(|e| error_string(&e.to_string()))?; + Ok(AegisWriteResult { + revision: rev.revision.as_u64(), + error: std::ptr::null_mut(), + }) + }, + )) { Ok(Ok(res)) => res, - Ok(Err(err)) => AegisWriteResult { revision: 0, error: err }, + Ok(Err(err)) => AegisWriteResult { + revision: 0, + error: err, + }, Err(panic) => { - let msg = panic.downcast_ref::<&str>().map(|s| s.to_string()) + let msg = panic + .downcast_ref::<&str>() + .map(|s| s.to_string()) .or_else(|| panic.downcast_ref::().cloned()) .unwrap_or_else(|| "unknown panic".to_string()); - AegisWriteResult { revision: 0, error: error_string(&msg) } + AegisWriteResult { + revision: 0, + error: error_string(&msg), + } } } } @@ -666,17 +773,20 @@ pub extern "C" fn aegis_engine_check_schema( Err(err) => return err, }; - match panic::catch_unwind(AssertUnwindSafe(|| -> Result<*mut libc::c_char, *mut libc::c_char> { - let yaml_str = c_str_to_str(schema_yaml)?; - let new_schema = parse_schema(&yaml_str).map_err(|e| error_string(&e.to_string()))?; - let report = eng.check_schema(&new_schema); - let json = serde_json::to_string(&serde_json::json!({ - "compatible": report.compatible, - "warnings": report.warnings, - "breaking": report.breaking, - })).map_err(|e| error_string(&e.to_string()))?; - Ok(error_string(&json)) - })) { + match panic::catch_unwind(AssertUnwindSafe( + || -> Result<*mut libc::c_char, *mut libc::c_char> { + let yaml_str = c_str_to_str(schema_yaml)?; + let new_schema = parse_schema(&yaml_str).map_err(|e| error_string(&e.to_string()))?; + let report = eng.check_schema(&new_schema); + let json = serde_json::to_string(&serde_json::json!({ + "compatible": report.compatible, + "warnings": report.warnings, + "breaking": report.breaking, + })) + .map_err(|e| error_string(&e.to_string()))?; + Ok(error_string(&json)) + }, + )) { Ok(Ok(ptr)) => ptr, Ok(Err(err)) => err, Err(_) => error_string("panic during check_schema"), @@ -703,26 +813,51 @@ pub extern "C" fn aegis_engine_check_dry_run( ) -> AegisCheckResult { let eng = match engine_from_ptr(engine) { Ok(e) => e, - Err(err) => return AegisCheckResult { allowed: false, revision: 0, error: err }, + Err(err) => { + return AegisCheckResult { + allowed: false, + revision: 0, + error: err, + }; + } }; - match panic::catch_unwind(AssertUnwindSafe(|| -> Result { - let subject_str = c_str_to_str(subject)?; - let permission_str = c_str_to_str(permission)?; - let resource_str = c_str_to_str(resource)?; - let subject_id = SubjectId::new(&subject_str).map_err(|e| error_string(&e.to_string()))?; - let resource_id = ResourceId::new(&resource_str).map_err(|e| error_string(&e.to_string()))?; - let result = eng.check_dry_run(&subject_id, &permission_str, &resource_id, None) - .map_err(|e| error_string(&e.to_string()))?; - Ok(AegisCheckResult { allowed: result.allowed, revision: result.revision.as_u64(), error: std::ptr::null_mut() }) - })) { + match panic::catch_unwind(AssertUnwindSafe( + || -> Result { + let subject_str = c_str_to_str(subject)?; + let permission_str = c_str_to_str(permission)?; + let resource_str = c_str_to_str(resource)?; + let subject_id = + SubjectId::new(&subject_str).map_err(|e| error_string(&e.to_string()))?; + let resource_id = + ResourceId::new(&resource_str).map_err(|e| error_string(&e.to_string()))?; + let result = eng + .check_dry_run(&subject_id, &permission_str, &resource_id, None) + .map_err(|e| error_string(&e.to_string()))?; + Ok(AegisCheckResult { + allowed: result.allowed, + revision: result.revision.as_u64(), + error: std::ptr::null_mut(), + }) + }, + )) { Ok(Ok(res)) => res, - Ok(Err(err)) => AegisCheckResult { allowed: false, revision: 0, error: err }, + Ok(Err(err)) => AegisCheckResult { + allowed: false, + revision: 0, + error: err, + }, Err(panic) => { - let msg = panic.downcast_ref::<&str>().map(|s| s.to_string()) + let msg = panic + .downcast_ref::<&str>() + .map(|s| s.to_string()) .or_else(|| panic.downcast_ref::().cloned()) .unwrap_or_else(|| "unknown panic".to_string()); - AegisCheckResult { allowed: false, revision: 0, error: error_string(&msg) } + AegisCheckResult { + allowed: false, + revision: 0, + error: error_string(&msg), + } } } } @@ -739,27 +874,52 @@ pub extern "C" fn aegis_engine_check_ex( ) -> AegisCheckResult { let eng = match engine_from_ptr(engine) { Ok(e) => e, - Err(err) => return AegisCheckResult { allowed: false, revision: 0, error: err }, - }; - - match panic::catch_unwind(AssertUnwindSafe(|| -> Result { - let subject_str = c_str_to_str(subject)?; - let permission_str = c_str_to_str(permission)?; - let resource_str = c_str_to_str(resource)?; - let subject_id = SubjectId::new(&subject_str).map_err(|e| error_string(&e.to_string()))?; - let resource_id = ResourceId::new(&resource_str).map_err(|e| error_string(&e.to_string()))?; - let cm = c_consistency(consistency); - let result = eng.check(&subject_id, &permission_str, &resource_id, cm) - .map_err(|e| error_string(&e.to_string()))?; - Ok(AegisCheckResult { allowed: result.allowed, revision: result.revision.as_u64(), error: std::ptr::null_mut() }) - })) { + Err(err) => { + return AegisCheckResult { + allowed: false, + revision: 0, + error: err, + }; + } + }; + + match panic::catch_unwind(AssertUnwindSafe( + || -> Result { + let subject_str = c_str_to_str(subject)?; + let permission_str = c_str_to_str(permission)?; + let resource_str = c_str_to_str(resource)?; + let subject_id = + SubjectId::new(&subject_str).map_err(|e| error_string(&e.to_string()))?; + let resource_id = + ResourceId::new(&resource_str).map_err(|e| error_string(&e.to_string()))?; + let cm = c_consistency(consistency); + let result = eng + .check(&subject_id, &permission_str, &resource_id, cm) + .map_err(|e| error_string(&e.to_string()))?; + Ok(AegisCheckResult { + allowed: result.allowed, + revision: result.revision.as_u64(), + error: std::ptr::null_mut(), + }) + }, + )) { Ok(Ok(res)) => res, - Ok(Err(err)) => AegisCheckResult { allowed: false, revision: 0, error: err }, + Ok(Err(err)) => AegisCheckResult { + allowed: false, + revision: 0, + error: err, + }, Err(panic) => { - let msg = panic.downcast_ref::<&str>().map(|s| s.to_string()) + let msg = panic + .downcast_ref::<&str>() + .map(|s| s.to_string()) .or_else(|| panic.downcast_ref::().cloned()) .unwrap_or_else(|| "unknown panic".to_string()); - AegisCheckResult { allowed: false, revision: 0, error: error_string(&msg) } + AegisCheckResult { + allowed: false, + revision: 0, + error: error_string(&msg), + } } } } @@ -777,50 +937,75 @@ pub extern "C" fn aegis_engine_check_with_context( ) -> AegisCheckResult { let eng = match engine_from_ptr(engine) { Ok(e) => e, - Err(err) => return AegisCheckResult { allowed: false, revision: 0, error: err }, - }; + Err(err) => { + return AegisCheckResult { + allowed: false, + revision: 0, + error: err, + }; + } + }; + + match panic::catch_unwind(AssertUnwindSafe( + || -> Result { + let subject_str = c_str_to_str(subject)?; + let permission_str = c_str_to_str(permission)?; + let resource_str = c_str_to_str(resource)?; + let subject_id = + SubjectId::new(&subject_str).map_err(|e| error_string(&e.to_string()))?; + let resource_id = + ResourceId::new(&resource_str).map_err(|e| error_string(&e.to_string()))?; + let cm = c_consistency(consistency); + + let ctx = if context_json.is_null() { + Default::default() + } else { + let json_str = c_str_to_str(context_json)?; + #[derive(serde::Deserialize)] + struct CtxJson { + #[serde(default)] + subject_meta: std::collections::HashMap, + #[serde(default)] + resource_meta: std::collections::HashMap, + #[serde(default)] + env: std::collections::HashMap, + } + let parsed: CtxJson = + serde_json::from_str(&json_str).map_err(|e| error_string(&e.to_string()))?; + aegis_core::engine::condition::ConditionEvalContext { + subject_meta: parsed.subject_meta, + resource_meta: parsed.resource_meta, + env: parsed.env, + } + }; - match panic::catch_unwind(AssertUnwindSafe(|| -> Result { - let subject_str = c_str_to_str(subject)?; - let permission_str = c_str_to_str(permission)?; - let resource_str = c_str_to_str(resource)?; - let subject_id = SubjectId::new(&subject_str).map_err(|e| error_string(&e.to_string()))?; - let resource_id = ResourceId::new(&resource_str).map_err(|e| error_string(&e.to_string()))?; - let cm = c_consistency(consistency); - - let ctx = if context_json.is_null() { - Default::default() - } else { - let json_str = c_str_to_str(context_json)?; - #[derive(serde::Deserialize)] - struct CtxJson { - #[serde(default)] - subject_meta: std::collections::HashMap, - #[serde(default)] - resource_meta: std::collections::HashMap, - #[serde(default)] - env: std::collections::HashMap, - } - let parsed: CtxJson = serde_json::from_str(&json_str) + let result = eng + .check_with_context(&subject_id, &permission_str, &resource_id, cm, ctx) .map_err(|e| error_string(&e.to_string()))?; - aegis_core::engine::condition::ConditionEvalContext { - subject_meta: parsed.subject_meta, - resource_meta: parsed.resource_meta, - env: parsed.env, - } - }; - - let result = eng.check_with_context(&subject_id, &permission_str, &resource_id, cm, ctx) - .map_err(|e| error_string(&e.to_string()))?; - Ok(AegisCheckResult { allowed: result.allowed, revision: result.revision.as_u64(), error: std::ptr::null_mut() }) - })) { + Ok(AegisCheckResult { + allowed: result.allowed, + revision: result.revision.as_u64(), + error: std::ptr::null_mut(), + }) + }, + )) { Ok(Ok(res)) => res, - Ok(Err(err)) => AegisCheckResult { allowed: false, revision: 0, error: err }, + Ok(Err(err)) => AegisCheckResult { + allowed: false, + revision: 0, + error: err, + }, Err(panic) => { - let msg = panic.downcast_ref::<&str>().map(|s| s.to_string()) + let msg = panic + .downcast_ref::<&str>() + .map(|s| s.to_string()) .or_else(|| panic.downcast_ref::().cloned()) .unwrap_or_else(|| "unknown panic".to_string()); - AegisCheckResult { allowed: false, revision: 0, error: error_string(&msg) } + AegisCheckResult { + allowed: false, + revision: 0, + error: error_string(&msg), + } } } } @@ -839,53 +1024,84 @@ pub extern "C" fn aegis_engine_write_ex( ) -> AegisWriteResult { let eng = match engine_from_ptr(engine) { Ok(e) => e, - Err(err) => return AegisWriteResult { revision: 0, error: err }, - }; - - match panic::catch_unwind(AssertUnwindSafe(|| -> Result { - let subject_str = c_str_to_str(subject)?; - let relation_str = c_str_to_str(relation)?; - let resource_str = c_str_to_str(resource)?; - let subject_id = SubjectId::new(&subject_str).map_err(|e| error_string(&e.to_string()))?; - let relation_id = Relation::new(&relation_str).map_err(|e| error_string(&e.to_string()))?; - let resource_id = ResourceId::new(&resource_str).map_err(|e| error_string(&e.to_string()))?; - - let condition_str = if condition.is_null() { None } else { Some(c_str_to_str(condition)?) }; - let metadata = if metadata_json.is_null() { - None - } else { - let json_str = c_str_to_str(metadata_json)?; - Some(serde_json::from_str::>(&json_str) - .map_err(|e| error_string(&e.to_string()))?) - }; - let valid_until_dt = if valid_until.is_null() { - None - } else { - let s = c_str_to_str(valid_until)?; - Some(chrono::DateTime::parse_from_rfc3339(&s) - .map_err(|e| error_string(&e.to_string()))? - .with_timezone(&chrono::Utc)) - }; - - let tuple = RelationshipTuple { - subject: subject_id, - relation: relation_id, - object: resource_id, - created_at: chrono::Utc::now(), - metadata, - valid_until: valid_until_dt, - condition: condition_str, - }; - let rev = eng.write(&tuple).map_err(|e| error_string(&e.to_string()))?; - Ok(AegisWriteResult { revision: rev.revision.as_u64(), error: std::ptr::null_mut() }) - })) { + Err(err) => { + return AegisWriteResult { + revision: 0, + error: err, + }; + } + }; + + match panic::catch_unwind(AssertUnwindSafe( + || -> Result { + let subject_str = c_str_to_str(subject)?; + let relation_str = c_str_to_str(relation)?; + let resource_str = c_str_to_str(resource)?; + let subject_id = + SubjectId::new(&subject_str).map_err(|e| error_string(&e.to_string()))?; + let relation_id = + Relation::new(&relation_str).map_err(|e| error_string(&e.to_string()))?; + let resource_id = + ResourceId::new(&resource_str).map_err(|e| error_string(&e.to_string()))?; + + let condition_str = if condition.is_null() { + None + } else { + Some(c_str_to_str(condition)?) + }; + let metadata = if metadata_json.is_null() { + None + } else { + let json_str = c_str_to_str(metadata_json)?; + Some( + serde_json::from_str::>(&json_str) + .map_err(|e| error_string(&e.to_string()))?, + ) + }; + let valid_until_dt = if valid_until.is_null() { + None + } else { + let s = c_str_to_str(valid_until)?; + Some( + chrono::DateTime::parse_from_rfc3339(&s) + .map_err(|e| error_string(&e.to_string()))? + .with_timezone(&chrono::Utc), + ) + }; + + let tuple = RelationshipTuple { + subject: subject_id, + relation: relation_id, + object: resource_id, + created_at: chrono::Utc::now(), + metadata, + valid_until: valid_until_dt, + condition: condition_str, + }; + let rev = eng + .write(&tuple) + .map_err(|e| error_string(&e.to_string()))?; + Ok(AegisWriteResult { + revision: rev.revision.as_u64(), + error: std::ptr::null_mut(), + }) + }, + )) { Ok(Ok(res)) => res, - Ok(Err(err)) => AegisWriteResult { revision: 0, error: err }, + Ok(Err(err)) => AegisWriteResult { + revision: 0, + error: err, + }, Err(panic) => { - let msg = panic.downcast_ref::<&str>().map(|s| s.to_string()) + let msg = panic + .downcast_ref::<&str>() + .map(|s| s.to_string()) .or_else(|| panic.downcast_ref::().cloned()) .unwrap_or_else(|| "unknown panic".to_string()); - AegisWriteResult { revision: 0, error: error_string(&msg) } + AegisWriteResult { + revision: 0, + error: error_string(&msg), + } } } } @@ -901,27 +1117,54 @@ pub extern "C" fn aegis_engine_write_dry_run( ) -> AegisCheckResult { let eng = match engine_from_ptr(engine) { Ok(e) => e, - Err(err) => return AegisCheckResult { allowed: false, revision: 0, error: err }, - }; - - match panic::catch_unwind(AssertUnwindSafe(|| -> Result { - let subject_str = c_str_to_str(subject)?; - let relation_str = c_str_to_str(relation)?; - let resource_str = c_str_to_str(resource)?; - let subject_id = SubjectId::new(&subject_str).map_err(|e| error_string(&e.to_string()))?; - let relation_id = Relation::new(&relation_str).map_err(|e| error_string(&e.to_string()))?; - let resource_id = ResourceId::new(&resource_str).map_err(|e| error_string(&e.to_string()))?; - let tuple = RelationshipTuple::new(subject_id, relation_id, resource_id); - let rev = eng.write_dry_run(&tuple).map_err(|e| error_string(&e.to_string()))?; - Ok(AegisCheckResult { allowed: false, revision: rev.revision.as_u64(), error: std::ptr::null_mut() }) - })) { + Err(err) => { + return AegisCheckResult { + allowed: false, + revision: 0, + error: err, + }; + } + }; + + match panic::catch_unwind(AssertUnwindSafe( + || -> Result { + let subject_str = c_str_to_str(subject)?; + let relation_str = c_str_to_str(relation)?; + let resource_str = c_str_to_str(resource)?; + let subject_id = + SubjectId::new(&subject_str).map_err(|e| error_string(&e.to_string()))?; + let relation_id = + Relation::new(&relation_str).map_err(|e| error_string(&e.to_string()))?; + let resource_id = + ResourceId::new(&resource_str).map_err(|e| error_string(&e.to_string()))?; + let tuple = RelationshipTuple::new(subject_id, relation_id, resource_id); + let rev = eng + .write_dry_run(&tuple) + .map_err(|e| error_string(&e.to_string()))?; + Ok(AegisCheckResult { + allowed: false, + revision: rev.revision.as_u64(), + error: std::ptr::null_mut(), + }) + }, + )) { Ok(Ok(res)) => res, - Ok(Err(err)) => AegisCheckResult { allowed: false, revision: 0, error: err }, + Ok(Err(err)) => AegisCheckResult { + allowed: false, + revision: 0, + error: err, + }, Err(panic) => { - let msg = panic.downcast_ref::<&str>().map(|s| s.to_string()) + let msg = panic + .downcast_ref::<&str>() + .map(|s| s.to_string()) .or_else(|| panic.downcast_ref::().cloned()) .unwrap_or_else(|| "unknown panic".to_string()); - AegisCheckResult { allowed: false, revision: 0, error: error_string(&msg) } + AegisCheckResult { + allowed: false, + revision: 0, + error: error_string(&msg), + } } } } @@ -935,26 +1178,54 @@ pub extern "C" fn aegis_engine_export_subject( ) -> AegisExportResult { let eng = match engine_from_ptr(engine) { Ok(e) => e, - Err(err) => return AegisExportResult { tuples_json: std::ptr::null_mut(), export_revision: 0, error: err }, + Err(err) => { + return AegisExportResult { + tuples_json: std::ptr::null_mut(), + export_revision: 0, + error: err, + }; + } }; - match panic::catch_unwind(AssertUnwindSafe(|| -> Result { - let subject_str = c_str_to_str(subject)?; - let subject_id = SubjectId::new(&subject_str).map_err(|e| error_string(&e.to_string()))?; - let tuples = eng.export_subject(&subject_id).map_err(|e| error_string(&e.to_string()))?; - let rev = eng.storage().current_revision(&PartitionId::default()).map_err(|e| error_string(&e.to_string()))?; - let json = serde_json::to_string(&tuples.iter().map(|t| { + match panic::catch_unwind(AssertUnwindSafe( + || -> Result { + let subject_str = c_str_to_str(subject)?; + let subject_id = + SubjectId::new(&subject_str).map_err(|e| error_string(&e.to_string()))?; + let tuples = eng + .export_subject(&subject_id) + .map_err(|e| error_string(&e.to_string()))?; + let rev = eng + .storage() + .current_revision(&PartitionId::default()) + .map_err(|e| error_string(&e.to_string()))?; + let json = serde_json::to_string(&tuples.iter().map(|t| { serde_json::json!({"subject": t.subject.as_str(), "relation": t.relation.as_str(), "object": t.object.as_str()}) }).collect::>()).map_err(|e| error_string(&e.to_string()))?; - Ok(AegisExportResult { tuples_json: error_string(&json), export_revision: rev.as_u64(), error: std::ptr::null_mut() }) - })) { + Ok(AegisExportResult { + tuples_json: error_string(&json), + export_revision: rev.as_u64(), + error: std::ptr::null_mut(), + }) + }, + )) { Ok(Ok(res)) => res, - Ok(Err(err)) => AegisExportResult { tuples_json: std::ptr::null_mut(), export_revision: 0, error: err }, + Ok(Err(err)) => AegisExportResult { + tuples_json: std::ptr::null_mut(), + export_revision: 0, + error: err, + }, Err(panic) => { - let msg = panic.downcast_ref::<&str>().map(|s| s.to_string()) + let msg = panic + .downcast_ref::<&str>() + .map(|s| s.to_string()) .or_else(|| panic.downcast_ref::().cloned()) .unwrap_or_else(|| "unknown panic".to_string()); - AegisExportResult { tuples_json: std::ptr::null_mut(), export_revision: 0, error: error_string(&msg) } + AegisExportResult { + tuples_json: std::ptr::null_mut(), + export_revision: 0, + error: error_string(&msg), + } } } } @@ -970,30 +1241,50 @@ pub extern "C" fn aegis_engine_delete_subject_with_policy( ) -> AegisWriteResult { let eng = match engine_from_ptr(engine) { Ok(e) => e, - Err(err) => return AegisWriteResult { revision: 0, error: err }, - }; - - match panic::catch_unwind(AssertUnwindSafe(|| -> Result { - let subject_str = c_str_to_str(subject)?; - let policy_str = c_str_to_str(policy)?; - let subject_id = SubjectId::new(&subject_str).map_err(|e| error_string(&e.to_string()))?; - let transfer = if transfer_to_subject.is_null() { - None - } else { - let t_str = c_str_to_str(transfer_to_subject)?; - Some(SubjectId::new(&t_str).map_err(|e| error_string(&e.to_string()))?) - }; - let rev = eng.delete_subject_with_policy(&subject_id, &policy_str, transfer.as_ref()) - .map_err(|e| error_string(&e.to_string()))?; - Ok(AegisWriteResult { revision: rev.revision.as_u64(), error: std::ptr::null_mut() }) - })) { + Err(err) => { + return AegisWriteResult { + revision: 0, + error: err, + }; + } + }; + + match panic::catch_unwind(AssertUnwindSafe( + || -> Result { + let subject_str = c_str_to_str(subject)?; + let policy_str = c_str_to_str(policy)?; + let subject_id = + SubjectId::new(&subject_str).map_err(|e| error_string(&e.to_string()))?; + let transfer = if transfer_to_subject.is_null() { + None + } else { + let t_str = c_str_to_str(transfer_to_subject)?; + Some(SubjectId::new(&t_str).map_err(|e| error_string(&e.to_string()))?) + }; + let rev = eng + .delete_subject_with_policy(&subject_id, &policy_str, transfer.as_ref()) + .map_err(|e| error_string(&e.to_string()))?; + Ok(AegisWriteResult { + revision: rev.revision.as_u64(), + error: std::ptr::null_mut(), + }) + }, + )) { Ok(Ok(res)) => res, - Ok(Err(err)) => AegisWriteResult { revision: 0, error: err }, + Ok(Err(err)) => AegisWriteResult { + revision: 0, + error: err, + }, Err(panic) => { - let msg = panic.downcast_ref::<&str>().map(|s| s.to_string()) + let msg = panic + .downcast_ref::<&str>() + .map(|s| s.to_string()) .or_else(|| panic.downcast_ref::().cloned()) .unwrap_or_else(|| "unknown panic".to_string()); - AegisWriteResult { revision: 0, error: error_string(&msg) } + AegisWriteResult { + revision: 0, + error: error_string(&msg), + } } } } @@ -1010,37 +1301,74 @@ pub extern "C" fn aegis_engine_query_audit( ) -> AegisAuditResult { let eng = match engine_from_ptr(engine) { Ok(e) => e, - Err(err) => return AegisAuditResult { entries_json: std::ptr::null_mut(), error: err }, - }; - - match panic::catch_unwind(AssertUnwindSafe(|| -> Result { - let object_str = c_str_to_str(object)?; - let object_id = ResourceId::new(&object_str).map_err(|e| error_string(&e.to_string()))?; - let from = if from_revision < 0 { None } else { Some(Revision::from(from_revision as u64)) }; - let to = if to_revision < 0 { None } else { Some(Revision::from(to_revision as u64)) }; - let pp = PaginationParams { limit, cursor: None }; - let entries = eng.query_audit(&object_id, from, to, &pp) + Err(err) => { + return AegisAuditResult { + entries_json: std::ptr::null_mut(), + error: err, + }; + } + }; + + match panic::catch_unwind(AssertUnwindSafe( + || -> Result { + let object_str = c_str_to_str(object)?; + let object_id = + ResourceId::new(&object_str).map_err(|e| error_string(&e.to_string()))?; + let from = if from_revision < 0 { + None + } else { + Some(Revision::from(from_revision as u64)) + }; + let to = if to_revision < 0 { + None + } else { + Some(Revision::from(to_revision as u64)) + }; + let pp = PaginationParams { + limit, + cursor: None, + }; + let entries = eng + .query_audit(&object_id, from, to, &pp) + .map_err(|e| error_string(&e.to_string()))?; + let json = serde_json::to_string( + &entries + .iter() + .map(|e| { + serde_json::json!({ + "revision": e.revision.as_u64(), + "action": format!("{:?}", e.action).to_lowercase(), + "subject": e.subject, + "relation": e.relation, + "object": e.object, + "timestamp": e.timestamp.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(), + "identity": e.identity, + }) + }) + .collect::>(), + ) .map_err(|e| error_string(&e.to_string()))?; - let json = serde_json::to_string(&entries.iter().map(|e| { - serde_json::json!({ - "revision": e.revision.as_u64(), - "action": format!("{:?}", e.action).to_lowercase(), - "subject": e.subject, - "relation": e.relation, - "object": e.object, - "timestamp": e.timestamp.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(), - "identity": e.identity, + Ok(AegisAuditResult { + entries_json: error_string(&json), + error: std::ptr::null_mut(), }) - }).collect::>()).map_err(|e| error_string(&e.to_string()))?; - Ok(AegisAuditResult { entries_json: error_string(&json), error: std::ptr::null_mut() }) - })) { + }, + )) { Ok(Ok(res)) => res, - Ok(Err(err)) => AegisAuditResult { entries_json: std::ptr::null_mut(), error: err }, + Ok(Err(err)) => AegisAuditResult { + entries_json: std::ptr::null_mut(), + error: err, + }, Err(panic) => { - let msg = panic.downcast_ref::<&str>().map(|s| s.to_string()) + let msg = panic + .downcast_ref::<&str>() + .map(|s| s.to_string()) .or_else(|| panic.downcast_ref::().cloned()) .unwrap_or_else(|| "unknown panic".to_string()); - AegisAuditResult { entries_json: std::ptr::null_mut(), error: error_string(&msg) } + AegisAuditResult { + entries_json: std::ptr::null_mut(), + error: error_string(&msg), + } } } } @@ -1055,28 +1383,49 @@ pub extern "C" fn aegis_engine_list_by_relation( ) -> AegisListResult { let eng = match engine_from_ptr(engine) { Ok(e) => e, - Err(err) => return AegisListResult { tuples_json: std::ptr::null_mut(), error: err }, + Err(err) => { + return AegisListResult { + tuples_json: std::ptr::null_mut(), + error: err, + }; + } }; - match panic::catch_unwind(AssertUnwindSafe(|| -> Result { - let object_str = c_str_to_str(object)?; - let relation_str = c_str_to_str(relation)?; - let object_id = ResourceId::new(&object_str).map_err(|e| error_string(&e.to_string()))?; - let relation_id = Relation::new(&relation_str).map_err(|e| error_string(&e.to_string()))?; - let tuples = eng.list_by_relation(&object_id, &relation_id) - .map_err(|e| error_string(&e.to_string()))?; - let json = serde_json::to_string(&tuples.iter().map(|t| { + match panic::catch_unwind(AssertUnwindSafe( + || -> Result { + let object_str = c_str_to_str(object)?; + let relation_str = c_str_to_str(relation)?; + let object_id = + ResourceId::new(&object_str).map_err(|e| error_string(&e.to_string()))?; + let relation_id = + Relation::new(&relation_str).map_err(|e| error_string(&e.to_string()))?; + let tuples = eng + .list_by_relation(&object_id, &relation_id) + .map_err(|e| error_string(&e.to_string()))?; + let json = serde_json::to_string(&tuples.iter().map(|t| { serde_json::json!({"subject": t.subject.as_str(), "relation": t.relation.as_str(), "object": t.object.as_str()}) }).collect::>()).map_err(|e| error_string(&e.to_string()))?; - Ok(AegisListResult { tuples_json: error_string(&json), error: std::ptr::null_mut() }) - })) { + Ok(AegisListResult { + tuples_json: error_string(&json), + error: std::ptr::null_mut(), + }) + }, + )) { Ok(Ok(res)) => res, - Ok(Err(err)) => AegisListResult { tuples_json: std::ptr::null_mut(), error: err }, + Ok(Err(err)) => AegisListResult { + tuples_json: std::ptr::null_mut(), + error: err, + }, Err(panic) => { - let msg = panic.downcast_ref::<&str>().map(|s| s.to_string()) + let msg = panic + .downcast_ref::<&str>() + .map(|s| s.to_string()) .or_else(|| panic.downcast_ref::().cloned()) .unwrap_or_else(|| "unknown panic".to_string()); - AegisListResult { tuples_json: std::ptr::null_mut(), error: error_string(&msg) } + AegisListResult { + tuples_json: std::ptr::null_mut(), + error: error_string(&msg), + } } } } @@ -1092,60 +1441,89 @@ pub extern "C" fn aegis_engine_query( ) -> AegisQueryResult { let eng = match engine_from_ptr(engine) { Ok(e) => e, - Err(err) => return AegisQueryResult { tuples_json: std::ptr::null_mut(), next_cursor: 0, revision: 0, error: err }, + Err(err) => { + return AegisQueryResult { + tuples_json: std::ptr::null_mut(), + next_cursor: 0, + revision: 0, + error: err, + }; + } }; - match panic::catch_unwind(AssertUnwindSafe(|| -> Result { - let json_str = c_str_to_str(filter_json)?; - #[derive(serde::Deserialize)] - struct FilterJson { - subject_type: Option, - relation: Option, - object_type: Option, - metadata_key: Option, - metadata_value: Option, - } - let f: FilterJson = serde_json::from_str(&json_str) - .map_err(|e| error_string(&e.to_string()))?; - let relation = match f.relation { - Some(r) => Some(Relation::new(&r).map_err(|e| error_string(&e.to_string()))?), - None => None, - }; - let tf = aegis_core::storage::TupleFilter { - subject_type: f.subject_type, - relation, - object_type: f.object_type, - metadata_key: f.metadata_key, - metadata_value: f.metadata_value, - ..Default::default() - }; - let current_rev = eng.storage().current_revision(&PartitionId::default()) - .map_err(|e| error_string(&e.to_string()))?; - let pp = PaginationParams { - limit, - cursor: if cursor_offset > 0 { - Some(aegis_core::types::PaginationCursor { offset: cursor_offset, revision: current_rev }) - } else { None }, - }; - let result = eng.query(&tf, &pp, None) - .map_err(|e| error_string(&e.to_string()))?; - let json = serde_json::to_string(&result.tuples.iter().map(|t| { + match panic::catch_unwind(AssertUnwindSafe( + || -> Result { + let json_str = c_str_to_str(filter_json)?; + #[derive(serde::Deserialize)] + struct FilterJson { + subject_type: Option, + relation: Option, + object_type: Option, + metadata_key: Option, + metadata_value: Option, + } + let f: FilterJson = + serde_json::from_str(&json_str).map_err(|e| error_string(&e.to_string()))?; + let relation = match f.relation { + Some(r) => Some(Relation::new(&r).map_err(|e| error_string(&e.to_string()))?), + None => None, + }; + let tf = aegis_core::storage::TupleFilter { + subject_type: f.subject_type, + relation, + object_type: f.object_type, + metadata_key: f.metadata_key, + metadata_value: f.metadata_value, + ..Default::default() + }; + let current_rev = eng + .storage() + .current_revision(&PartitionId::default()) + .map_err(|e| error_string(&e.to_string()))?; + let pp = PaginationParams { + limit, + cursor: if cursor_offset > 0 { + Some(aegis_core::types::PaginationCursor { + offset: cursor_offset, + revision: current_rev, + }) + } else { + None + }, + }; + let result = eng + .query(&tf, &pp, None) + .map_err(|e| error_string(&e.to_string()))?; + let json = serde_json::to_string(&result.tuples.iter().map(|t| { serde_json::json!({"subject": t.subject.as_str(), "relation": t.relation.as_str(), "object": t.object.as_str()}) }).collect::>()).map_err(|e| error_string(&e.to_string()))?; - Ok(AegisQueryResult { - tuples_json: error_string(&json), - next_cursor: result.next_cursor.map(|c| c.offset).unwrap_or(0), - revision: result.revision.as_u64(), - error: std::ptr::null_mut(), - }) - })) { + Ok(AegisQueryResult { + tuples_json: error_string(&json), + next_cursor: result.next_cursor.map(|c| c.offset).unwrap_or(0), + revision: result.revision.as_u64(), + error: std::ptr::null_mut(), + }) + }, + )) { Ok(Ok(res)) => res, - Ok(Err(err)) => AegisQueryResult { tuples_json: std::ptr::null_mut(), next_cursor: 0, revision: 0, error: err }, + Ok(Err(err)) => AegisQueryResult { + tuples_json: std::ptr::null_mut(), + next_cursor: 0, + revision: 0, + error: err, + }, Err(panic) => { - let msg = panic.downcast_ref::<&str>().map(|s| s.to_string()) + let msg = panic + .downcast_ref::<&str>() + .map(|s| s.to_string()) .or_else(|| panic.downcast_ref::().cloned()) .unwrap_or_else(|| "unknown panic".to_string()); - AegisQueryResult { tuples_json: std::ptr::null_mut(), next_cursor: 0, revision: 0, error: error_string(&msg) } + AegisQueryResult { + tuples_json: std::ptr::null_mut(), + next_cursor: 0, + revision: 0, + error: error_string(&msg), + } } } } @@ -1162,12 +1540,15 @@ pub extern "C" fn aegis_engine_reload_schema( Err(err) => return err, }; - match panic::catch_unwind(AssertUnwindSafe(|| -> Result<*mut libc::c_char, *mut libc::c_char> { - let yaml_str = c_str_to_str(schema_yaml)?; - let new_schema = parse_schema(&yaml_str).map_err(|e| error_string(&e.to_string()))?; - eng.reload_schema(new_schema).map_err(|e| error_string(&e.to_string()))?; - Ok(std::ptr::null_mut()) - })) { + match panic::catch_unwind(AssertUnwindSafe( + || -> Result<*mut libc::c_char, *mut libc::c_char> { + let yaml_str = c_str_to_str(schema_yaml)?; + let new_schema = parse_schema(&yaml_str).map_err(|e| error_string(&e.to_string()))?; + eng.reload_schema(new_schema) + .map_err(|e| error_string(&e.to_string()))?; + Ok(std::ptr::null_mut()) + }, + )) { Ok(Ok(ptr)) => ptr, Ok(Err(err)) => err, Err(_) => error_string("panic during reload_schema"), @@ -1177,18 +1558,18 @@ pub extern "C" fn aegis_engine_reload_schema( // ── Close ── #[unsafe(no_mangle)] -pub extern "C" fn aegis_engine_close( - engine: *mut AegisEngine, -) -> *mut libc::c_char { +pub extern "C" fn aegis_engine_close(engine: *mut AegisEngine) -> *mut libc::c_char { let eng = match engine_from_ptr(engine) { Ok(e) => e, Err(err) => return err, }; - match panic::catch_unwind(AssertUnwindSafe(|| -> Result<*mut libc::c_char, *mut libc::c_char> { - eng.close().map_err(|e| error_string(&e.to_string()))?; - Ok(std::ptr::null_mut()) - })) { + match panic::catch_unwind(AssertUnwindSafe( + || -> Result<*mut libc::c_char, *mut libc::c_char> { + eng.close().map_err(|e| error_string(&e.to_string()))?; + Ok(std::ptr::null_mut()) + }, + )) { Ok(Ok(ptr)) => ptr, Ok(Err(err)) => err, Err(_) => error_string("panic during close"), @@ -1198,9 +1579,7 @@ pub extern "C" fn aegis_engine_close( // ── Is closed ── #[unsafe(no_mangle)] -pub extern "C" fn aegis_engine_is_closed( - engine: *const AegisEngine, -) -> bool { +pub extern "C" fn aegis_engine_is_closed(engine: *const AegisEngine) -> bool { if engine.is_null() { return true; } @@ -1220,33 +1599,49 @@ pub extern "C" fn aegis_engine_set_rate_limiter( Err(err) => return err, }; - let result = panic::catch_unwind(AssertUnwindSafe(|| -> Result<*mut libc::c_char, *mut libc::c_char> { - let json_str = c_str_to_str(config_json)?; - #[derive(serde::Deserialize)] - struct RlConfigJson { - checks_per_second: Option, - check_burst: Option, - writes_per_second: Option, - write_burst: Option, - max_traversal_depth: Option, - max_traversal_visits: Option, - max_keys: Option, - } - let parsed: RlConfigJson = serde_json::from_str(&json_str) - .map_err(|e| error_string(&e.to_string()))?; + let result = panic::catch_unwind(AssertUnwindSafe( + || -> Result<*mut libc::c_char, *mut libc::c_char> { + let json_str = c_str_to_str(config_json)?; + #[derive(serde::Deserialize)] + struct RlConfigJson { + checks_per_second: Option, + check_burst: Option, + writes_per_second: Option, + write_burst: Option, + max_traversal_depth: Option, + max_traversal_visits: Option, + max_keys: Option, + } + let parsed: RlConfigJson = + serde_json::from_str(&json_str).map_err(|e| error_string(&e.to_string()))?; - let mut cfg = RateLimitConfig::default(); - if let Some(v) = parsed.checks_per_second { cfg.checks_per_second = v; } - if let Some(v) = parsed.check_burst { cfg.check_burst = v; } - if let Some(v) = parsed.writes_per_second { cfg.writes_per_second = v; } - if let Some(v) = parsed.write_burst { cfg.write_burst = v; } - if let Some(v) = parsed.max_traversal_depth { cfg.max_traversal_depth = v; } - if let Some(v) = parsed.max_traversal_visits { cfg.max_traversal_visits = v; } - if let Some(v) = parsed.max_keys { cfg.max_keys = v; } - - eng.set_rate_limiter(TokenBucketRateLimiter::new(cfg)); - Ok(std::ptr::null_mut()) - })); + let mut cfg = RateLimitConfig::default(); + if let Some(v) = parsed.checks_per_second { + cfg.checks_per_second = v; + } + if let Some(v) = parsed.check_burst { + cfg.check_burst = v; + } + if let Some(v) = parsed.writes_per_second { + cfg.writes_per_second = v; + } + if let Some(v) = parsed.write_burst { + cfg.write_burst = v; + } + if let Some(v) = parsed.max_traversal_depth { + cfg.max_traversal_depth = v; + } + if let Some(v) = parsed.max_traversal_visits { + cfg.max_traversal_visits = v; + } + if let Some(v) = parsed.max_keys { + cfg.max_keys = v; + } + + eng.set_rate_limiter(TokenBucketRateLimiter::new(cfg)); + Ok(std::ptr::null_mut()) + }, + )); match result { Ok(Ok(ptr)) => ptr, @@ -1267,15 +1662,17 @@ pub extern "C" fn aegis_engine_set_actor( Err(err) => return err, }; - match panic::catch_unwind(AssertUnwindSafe(|| -> Result<*mut libc::c_char, *mut libc::c_char> { - if actor.is_null() { - eng.set_actor(None); - } else { - let s = c_str_to_str(actor)?; - eng.set_actor(Some(&s)); - } - Ok(std::ptr::null_mut()) - })) { + match panic::catch_unwind(AssertUnwindSafe( + || -> Result<*mut libc::c_char, *mut libc::c_char> { + if actor.is_null() { + eng.set_actor(None); + } else { + let s = c_str_to_str(actor)?; + eng.set_actor(Some(&s)); + } + Ok(std::ptr::null_mut()) + }, + )) { Ok(Ok(ptr)) => ptr, Ok(Err(err)) => err, Err(_) => error_string("panic during set_actor"), @@ -1283,9 +1680,7 @@ pub extern "C" fn aegis_engine_set_actor( } #[unsafe(no_mangle)] -pub extern "C" fn aegis_engine_active_actor( - engine: *const AegisEngine, -) -> *mut libc::c_char { +pub extern "C" fn aegis_engine_active_actor(engine: *const AegisEngine) -> *mut libc::c_char { let eng = match engine_from_const_ptr(engine) { Ok(e) => e, Err(err) => return err, @@ -1299,7 +1694,12 @@ pub extern "C" fn aegis_engine_active_actor( // ── Logger ── -pub type AegisLogFn = unsafe extern "C" fn(level: i32, target: *const libc::c_char, msg: *const libc::c_char, user_data: *mut libc::c_void); +pub type AegisLogFn = unsafe extern "C" fn( + level: i32, + target: *const libc::c_char, + msg: *const libc::c_char, + user_data: *mut libc::c_void, +); #[unsafe(no_mangle)] pub extern "C" fn aegis_engine_set_logger( @@ -1315,20 +1715,26 @@ pub extern "C" fn aegis_engine_set_logger( match callback { Some(cb) => { let ud = user_data as usize; - let wrapped: hooks::LoggerFn = Box::new(move |level: hooks::LogLevel, target: &str, msg: &str| { - let level_i32 = match level { - hooks::LogLevel::Error => 0, - hooks::LogLevel::Warn => 1, - hooks::LogLevel::Info => 2, - hooks::LogLevel::Debug => 3, - hooks::LogLevel::Trace => 4, - }; - let c_target = CString::new(target).unwrap_or_default(); - let c_msg = CString::new(msg).unwrap_or_default(); - unsafe { - cb(level_i32, c_target.as_ptr(), c_msg.as_ptr(), ud as *mut libc::c_void); - } - }); + let wrapped: hooks::LoggerFn = + Box::new(move |level: hooks::LogLevel, target: &str, msg: &str| { + let level_i32 = match level { + hooks::LogLevel::Error => 0, + hooks::LogLevel::Warn => 1, + hooks::LogLevel::Info => 2, + hooks::LogLevel::Debug => 3, + hooks::LogLevel::Trace => 4, + }; + let c_target = CString::new(target).unwrap_or_default(); + let c_msg = CString::new(msg).unwrap_or_default(); + unsafe { + cb( + level_i32, + c_target.as_ptr(), + c_msg.as_ptr(), + ud as *mut libc::c_void, + ); + } + }); eng.set_logger(wrapped); } None => { @@ -1369,9 +1775,36 @@ pub extern "C" fn aegis_engine_watch( let result = panic::catch_unwind(AssertUnwindSafe(|| -> *mut AegisWatchSubscription { let filter = WatchFilter { - subjects: if subject_type.is_null() { None } else { Some(vec![unsafe { CStr::from_ptr(subject_type) }.to_str().unwrap_or_default().to_string()]) }, - relations: if relation.is_null() { None } else { Some(vec![unsafe { CStr::from_ptr(relation) }.to_str().unwrap_or_default().to_string()]) }, - objects: if object_type.is_null() { None } else { Some(vec![unsafe { CStr::from_ptr(object_type) }.to_str().unwrap_or_default().to_string()]) }, + subjects: if subject_type.is_null() { + None + } else { + Some(vec![ + unsafe { CStr::from_ptr(subject_type) } + .to_str() + .unwrap_or_default() + .to_string(), + ]) + }, + relations: if relation.is_null() { + None + } else { + Some(vec![ + unsafe { CStr::from_ptr(relation) } + .to_str() + .unwrap_or_default() + .to_string(), + ]) + }, + objects: if object_type.is_null() { + None + } else { + Some(vec![ + unsafe { CStr::from_ptr(object_type) } + .to_str() + .unwrap_or_default() + .to_string(), + ]) + }, event_types: None, }; let sub = eng.watch(filter); @@ -1385,10 +1818,10 @@ pub extern "C" fn aegis_engine_watch( } #[unsafe(no_mangle)] -pub extern "C" fn aegis_watch_poll( - sub: *mut AegisWatchSubscription, -) -> *mut AegisWatchEvent { - if sub.is_null() { return std::ptr::null_mut(); } +pub extern "C" fn aegis_watch_poll(sub: *mut AegisWatchSubscription) -> *mut AegisWatchEvent { + if sub.is_null() { + return std::ptr::null_mut(); + } let sub = unsafe { &*sub }; let event = match &sub.inner { @@ -1415,6 +1848,7 @@ pub extern "C" fn aegis_watch_poll( .map(|v| error_string(&v.to_string())) .unwrap_or(std::ptr::null_mut()); + #[allow(clippy::let_and_return)] let ptr = Box::into_raw(Box::new(AegisWatchEvent { event_type: evt_type, subject: error_string(&event.subject), @@ -1439,12 +1873,36 @@ pub extern "C" fn aegis_watch_free(sub: *mut AegisWatchSubscription) { pub extern "C" fn aegis_watch_event_free(evt: *mut AegisWatchEvent) { if !evt.is_null() { let evt = unsafe { Box::from_raw(evt) }; - if !evt.subject.is_null() { unsafe { let _ = CString::from_raw(evt.subject); } } - if !evt.relation.is_null() { unsafe { let _ = CString::from_raw(evt.relation); } } - if !evt.object.is_null() { unsafe { let _ = CString::from_raw(evt.object); } } - if !evt.timestamp.is_null() { unsafe { let _ = CString::from_raw(evt.timestamp); } } - if !evt.payload.is_null() { unsafe { let _ = CString::from_raw(evt.payload); } } - if !evt.error.is_null() { unsafe { let _ = CString::from_raw(evt.error); } } + if !evt.subject.is_null() { + unsafe { + let _ = CString::from_raw(evt.subject); + } + } + if !evt.relation.is_null() { + unsafe { + let _ = CString::from_raw(evt.relation); + } + } + if !evt.object.is_null() { + unsafe { + let _ = CString::from_raw(evt.object); + } + } + if !evt.timestamp.is_null() { + unsafe { + let _ = CString::from_raw(evt.timestamp); + } + } + if !evt.payload.is_null() { + unsafe { + let _ = CString::from_raw(evt.payload); + } + } + if !evt.error.is_null() { + unsafe { + let _ = CString::from_raw(evt.error); + } + } } } @@ -1464,13 +1922,17 @@ pub extern "C" fn aegis_engine_transaction_begin( Err(_) => return std::ptr::null_mut(), }; - let result = panic::catch_unwind(AssertUnwindSafe(|| -> Result<*mut AegisTransaction, *mut libc::c_char> { - let txn = eng.transaction().map_err(|e| error_string(&e.to_string()))?; - Ok(Box::into_raw(Box::new(AegisTransaction { - inner: Mutex::new(Some(txn)), - consumed: AtomicBool::new(false), - }))) - })); + let result = panic::catch_unwind(AssertUnwindSafe( + || -> Result<*mut AegisTransaction, *mut libc::c_char> { + let txn = eng + .transaction() + .map_err(|e| error_string(&e.to_string()))?; + Ok(Box::into_raw(Box::new(AegisTransaction { + inner: Mutex::new(Some(txn)), + consumed: AtomicBool::new(false), + }))) + }, + )); match result { Ok(Ok(ptr)) => ptr, @@ -1501,9 +1963,13 @@ pub extern "C" fn aegis_transaction_write( relation: *const libc::c_char, resource: *const libc::c_char, ) -> *mut libc::c_char { - if txn.is_null() { return error_string("transaction is null"); } + if txn.is_null() { + return error_string("transaction is null"); + } let txn = unsafe { &*txn }; - if let Err(err) = txn_check_open(txn) { return err; } + if let Err(err) = txn_check_open(txn) { + return err; + } let result = panic::catch_unwind(AssertUnwindSafe(|| -> Result<(), *mut libc::c_char> { let subject_str = c_str_to_str(subject)?; @@ -1515,8 +1981,12 @@ pub extern "C" fn aegis_transaction_write( ResourceId::new(&resource_str).map_err(|e| error_string(&e.to_string()))?, ); let mut guard = txn.inner.lock().map_err(|e| error_string(&e.to_string()))?; - let inner = guard.as_mut().ok_or_else(|| error_string("transaction not initialized"))?; - inner.write(&PartitionId::default(), &tuple).map_err(|e| error_string(&e.to_string()))?; + let inner = guard + .as_mut() + .ok_or_else(|| error_string("transaction not initialized"))?; + inner + .write(&PartitionId::default(), &tuple) + .map_err(|e| error_string(&e.to_string()))?; Ok(()) })); @@ -1534,9 +2004,13 @@ pub extern "C" fn aegis_transaction_delete( relation: *const libc::c_char, resource: *const libc::c_char, ) -> *mut libc::c_char { - if txn.is_null() { return error_string("transaction is null"); } + if txn.is_null() { + return error_string("transaction is null"); + } let txn = unsafe { &*txn }; - if let Err(err) = txn_check_open(txn) { return err; } + if let Err(err) = txn_check_open(txn) { + return err; + } let result = panic::catch_unwind(AssertUnwindSafe(|| -> Result<(), *mut libc::c_char> { let subject_str = c_str_to_str(subject)?; @@ -1548,8 +2022,12 @@ pub extern "C" fn aegis_transaction_delete( object: ResourceId::new(&resource_str).map_err(|e| error_string(&e.to_string()))?, }; let mut guard = txn.inner.lock().map_err(|e| error_string(&e.to_string()))?; - let inner = guard.as_mut().ok_or_else(|| error_string("transaction not initialized"))?; - inner.delete(&PartitionId::default(), &key).map_err(|e| error_string(&e.to_string()))?; + let inner = guard + .as_mut() + .ok_or_else(|| error_string("transaction not initialized"))?; + inner + .delete(&PartitionId::default(), &key) + .map_err(|e| error_string(&e.to_string()))?; Ok(()) })); @@ -1565,15 +2043,23 @@ pub extern "C" fn aegis_transaction_savepoint( txn: *mut AegisTransaction, name: *const libc::c_char, ) -> *mut libc::c_char { - if txn.is_null() { return error_string("transaction is null"); } + if txn.is_null() { + return error_string("transaction is null"); + } let txn = unsafe { &*txn }; - if let Err(err) = txn_check_open(txn) { return err; } + if let Err(err) = txn_check_open(txn) { + return err; + } let result = panic::catch_unwind(AssertUnwindSafe(|| -> Result<(), *mut libc::c_char> { let name_str = c_str_to_str(name)?; let guard = txn.inner.lock().map_err(|e| error_string(&e.to_string()))?; - let inner = guard.as_ref().ok_or_else(|| error_string("transaction not initialized"))?; - inner.savepoint(&name_str).map_err(|e| error_string(&e.to_string()))?; + let inner = guard + .as_ref() + .ok_or_else(|| error_string("transaction not initialized"))?; + inner + .savepoint(&name_str) + .map_err(|e| error_string(&e.to_string()))?; Ok(()) })); @@ -1589,15 +2075,23 @@ pub extern "C" fn aegis_transaction_rollback_to_savepoint( txn: *mut AegisTransaction, name: *const libc::c_char, ) -> *mut libc::c_char { - if txn.is_null() { return error_string("transaction is null"); } + if txn.is_null() { + return error_string("transaction is null"); + } let txn = unsafe { &*txn }; - if let Err(err) = txn_check_open(txn) { return err; } + if let Err(err) = txn_check_open(txn) { + return err; + } let result = panic::catch_unwind(AssertUnwindSafe(|| -> Result<(), *mut libc::c_char> { let name_str = c_str_to_str(name)?; let guard = txn.inner.lock().map_err(|e| error_string(&e.to_string()))?; - let inner = guard.as_ref().ok_or_else(|| error_string("transaction not initialized"))?; - inner.rollback_to_savepoint(&name_str).map_err(|e| error_string(&e.to_string()))?; + let inner = guard + .as_ref() + .ok_or_else(|| error_string("transaction not initialized"))?; + inner + .rollback_to_savepoint(&name_str) + .map_err(|e| error_string(&e.to_string()))?; Ok(()) })); @@ -1613,15 +2107,23 @@ pub extern "C" fn aegis_transaction_release_savepoint( txn: *mut AegisTransaction, name: *const libc::c_char, ) -> *mut libc::c_char { - if txn.is_null() { return error_string("transaction is null"); } + if txn.is_null() { + return error_string("transaction is null"); + } let txn = unsafe { &*txn }; - if let Err(err) = txn_check_open(txn) { return err; } + if let Err(err) = txn_check_open(txn) { + return err; + } let result = panic::catch_unwind(AssertUnwindSafe(|| -> Result<(), *mut libc::c_char> { let name_str = c_str_to_str(name)?; let guard = txn.inner.lock().map_err(|e| error_string(&e.to_string()))?; - let inner = guard.as_ref().ok_or_else(|| error_string("transaction not initialized"))?; - inner.release_savepoint(&name_str).map_err(|e| error_string(&e.to_string()))?; + let inner = guard + .as_ref() + .ok_or_else(|| error_string("transaction not initialized"))?; + inner + .release_savepoint(&name_str) + .map_err(|e| error_string(&e.to_string()))?; Ok(()) })); @@ -1633,43 +2135,64 @@ pub extern "C" fn aegis_transaction_release_savepoint( } #[unsafe(no_mangle)] -pub extern "C" fn aegis_transaction_commit( - txn: *mut AegisTransaction, -) -> AegisWriteResult { +pub extern "C" fn aegis_transaction_commit(txn: *mut AegisTransaction) -> AegisWriteResult { if txn.is_null() { - return AegisWriteResult { revision: 0, error: error_string("transaction is null") }; + return AegisWriteResult { + revision: 0, + error: error_string("transaction is null"), + }; } let txn = unsafe { &*txn }; if let Err(err) = txn_check_open(txn) { - return AegisWriteResult { revision: 0, error: err }; + return AegisWriteResult { + revision: 0, + error: err, + }; } - let result = panic::catch_unwind(AssertUnwindSafe(|| -> Result { - let mut guard = txn.inner.lock().map_err(|e| error_string(&e.to_string()))?; - let inner = guard.take().ok_or_else(|| error_string("transaction not initialized"))?; - let revision = inner.commit().map_err(|e| error_string(&e.to_string()))?; - txn.consumed.store(true, Ordering::Relaxed); - Ok(AegisWriteResult { revision: revision.as_u64(), error: std::ptr::null_mut() }) - })); + let result = panic::catch_unwind(AssertUnwindSafe( + || -> Result { + let mut guard = txn.inner.lock().map_err(|e| error_string(&e.to_string()))?; + let inner = guard + .take() + .ok_or_else(|| error_string("transaction not initialized"))?; + let revision = inner.commit().map_err(|e| error_string(&e.to_string()))?; + txn.consumed.store(true, Ordering::Relaxed); + Ok(AegisWriteResult { + revision: revision.as_u64(), + error: std::ptr::null_mut(), + }) + }, + )); match result { Ok(Ok(res)) => res, - Ok(Err(err)) => AegisWriteResult { revision: 0, error: err }, - Err(_) => AegisWriteResult { revision: 0, error: error_string("panic during transaction_commit") }, + Ok(Err(err)) => AegisWriteResult { + revision: 0, + error: err, + }, + Err(_) => AegisWriteResult { + revision: 0, + error: error_string("panic during transaction_commit"), + }, } } #[unsafe(no_mangle)] -pub extern "C" fn aegis_transaction_rollback( - txn: *mut AegisTransaction, -) -> *mut libc::c_char { - if txn.is_null() { return error_string("transaction is null"); } +pub extern "C" fn aegis_transaction_rollback(txn: *mut AegisTransaction) -> *mut libc::c_char { + if txn.is_null() { + return error_string("transaction is null"); + } let txn = unsafe { &*txn }; - if let Err(err) = txn_check_open(txn) { return err; } + if let Err(err) = txn_check_open(txn) { + return err; + } let result = panic::catch_unwind(AssertUnwindSafe(|| -> Result<(), *mut libc::c_char> { let mut guard = txn.inner.lock().map_err(|e| error_string(&e.to_string()))?; - let inner = guard.take().ok_or_else(|| error_string("transaction not initialized"))?; + let inner = guard + .take() + .ok_or_else(|| error_string("transaction not initialized"))?; inner.rollback().map_err(|e| error_string(&e.to_string()))?; txn.consumed.store(true, Ordering::Relaxed); Ok(()) @@ -1686,7 +2209,9 @@ pub extern "C" fn aegis_transaction_rollback( pub extern "C" fn aegis_transaction_free(txn: *mut AegisTransaction) { if !txn.is_null() { let txn = unsafe { Box::from_raw(txn) }; + #[allow(clippy::collapsible_if)] if !txn.consumed.load(Ordering::Relaxed) { + #[allow(clippy::collapsible_if)] if let Ok(mut guard) = txn.inner.lock() { if let Some(inner) = guard.take() { let _ = inner.rollback(); @@ -1789,9 +2314,19 @@ pub extern "C" fn aegis_engine_who_can_access( }; let pagination = PaginationParams { limit: page_limit, - cursor: Some(PaginationCursor { offset: page_offset, revision: Revision::from(0) }), - }; - match eng.who_can_access(&permission_str, &resource_id, &pagination, include_paths, 10, 5000) { + cursor: Some(PaginationCursor { + offset: page_offset, + revision: Revision::from(0), + }), + }; + match eng.who_can_access( + &permission_str, + &resource_id, + &pagination, + include_paths, + 10, + 5000, + ) { Ok(result) => { let json = serde_json::to_string(&result).unwrap_or_default(); CString::new(json).unwrap_or_default().into_raw() @@ -1827,7 +2362,11 @@ pub extern "C" fn aegis_engine_access_diff( Ok(s) => s, Err(e) => return error_string(&e.to_string()), }; - let mc = if max_checks > 0 { Some(max_checks as u64) } else { None }; + let mc = if max_checks > 0 { + Some(max_checks as u64) + } else { + None + }; match eng.access_diff(&schema_before, &schema_after, None, mc) { Ok(result) => { let json = serde_json::to_string(&result).unwrap_or_default(); @@ -2151,10 +2690,11 @@ pub extern "C" fn aegis_engine_create_analysis_schedule( Ok(s) => s, Err(e) => return e, }; - let queries: Vec = match serde_json::from_str(&queries_json) { - Ok(q) => q, - Err(e) => return error_string(&format!("invalid queries: {}", e)), - }; + let queries: Vec = + match serde_json::from_str(&queries_json) { + Ok(q) => q, + Err(e) => return error_string(&format!("invalid queries: {}", e)), + }; let compare_schema = if compare_schema_json.is_null() { None } else { @@ -2336,7 +2876,9 @@ fn engine_from_ptr(ptr: *mut AegisEngine) -> Result<&'static GraphEngine, *mut l } } -fn engine_from_const_ptr(ptr: *const AegisEngine) -> Result<&'static GraphEngine, *mut libc::c_char> { +fn engine_from_const_ptr( + ptr: *const AegisEngine, +) -> Result<&'static GraphEngine, *mut libc::c_char> { if ptr.is_null() { Err(error_string("engine is null")) } else { diff --git a/crates/aegis-napi/src/lib.rs b/crates/aegis-napi/src/lib.rs index c58d590..cf72e4b 100644 --- a/crates/aegis-napi/src/lib.rs +++ b/crates/aegis-napi/src/lib.rs @@ -2,13 +2,13 @@ use std::collections::HashMap; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; -use napi::threadsafe_function::{ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode}; use aegis_core::engine::hooks::LogLevel; +use napi::threadsafe_function::{ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode}; +use aegis_core::engine::GraphEngine; use aegis_core::engine::condition::ConditionEvalContext; use aegis_core::engine::ratelimit::{RateLimitConfig, TokenBucketRateLimiter}; use aegis_core::engine::watch::{WatchEvent, WatchEventType, WatchFilter, WatchSubscription}; -use aegis_core::engine::GraphEngine; use aegis_core::schema::parse_schema; use aegis_core::storage::sqlite::{SqliteConfig, SqliteStorage}; use aegis_core::storage::{StorageBackend, StorageTransaction, TupleFilter}; @@ -47,12 +47,15 @@ fn parse_consistency(s: Option) -> napi::Result> } Some(ref val) => { if let Some(rev_str) = val.strip_prefix("at_revision:") { - let rev_num: u64 = rev_str - .parse() - .map_err(|_| napi::Error::from_reason(format!("invalid consistency: {}", val)))?; + let rev_num: u64 = rev_str.parse().map_err(|_| { + napi::Error::from_reason(format!("invalid consistency: {}", val)) + })?; Ok(Some(ConsistencyMode::AtRevision(Revision::from(rev_num)))) } else { - Err(napi::Error::from_reason(format!("invalid consistency: {}", val))) + Err(napi::Error::from_reason(format!( + "invalid consistency: {}", + val + ))) } } } @@ -63,7 +66,8 @@ fn validate_tuple( relation: &str, resource: &str, ) -> napi::Result { - let subject_id = SubjectId::new(subject).map_err(|e| napi::Error::from_reason(e.to_string()))?; + let subject_id = + SubjectId::new(subject).map_err(|e| napi::Error::from_reason(e.to_string()))?; let relation_id = Relation::new(relation).map_err(|e| napi::Error::from_reason(e.to_string()))?; let resource_id = @@ -231,10 +235,7 @@ fn revision_token_to_nap(token: &aegis_core::types::RevisionToken) -> WriteResul WriteResultNAP { revision: token.revision.as_u64() as i64, node_id: token.node_id.to_string(), - timestamp: token - .timestamp - .format("%Y-%m-%dT%H:%M:%S%.3fZ") - .to_string(), + timestamp: token.timestamp.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(), } } @@ -245,10 +246,7 @@ fn audit_entry_to_nap(entry: &AuditEntry) -> AuditEntryNAP { subject: entry.subject.clone(), relation: entry.relation.clone(), object: entry.object.clone(), - timestamp: entry - .timestamp - .format("%Y-%m-%dT%H:%M:%S%.3fZ") - .to_string(), + timestamp: entry.timestamp.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(), identity: entry.identity.clone(), } } @@ -260,7 +258,9 @@ fn tuple_to_nap(tuple: &RelationshipTuple) -> TupleNAP { object: tuple.object.as_str().to_string(), condition: tuple.condition.clone(), metadata: tuple.metadata.clone(), - valid_until: tuple.valid_until.map(|dt| dt.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string()), + valid_until: tuple + .valid_until + .map(|dt| dt.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string()), } } @@ -301,10 +301,7 @@ fn watch_event_to_nap(event: &WatchEvent) -> WatchEventNAP { relation: event.relation.clone(), object: event.object.clone(), revision: event.revision.as_u64() as i64, - timestamp: event - .timestamp - .format("%Y-%m-%dT%H:%M:%S%.3fZ") - .to_string(), + timestamp: event.timestamp.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(), } } @@ -334,7 +331,11 @@ impl JsAegis { // ── Factory ─────────────────────────────────────────────────────────────────────────── #[napi] -pub fn initialize(path: String, schema_yaml: String, config: Option) -> napi::Result { +pub fn initialize( + path: String, + schema_yaml: String, + config: Option, +) -> napi::Result { catch_engine_panic(move || { let cfg = config.unwrap_or(EngineConfigNAP { max_readers: None, @@ -354,8 +355,8 @@ pub fn initialize(path: String, schema_yaml: String, config: Option napi::Result { self.check_open()?; catch_engine_panic(|| { - let subject_id = SubjectId::new(&subject) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; - let resource_id = ResourceId::new(&resource) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; + let subject_id = + SubjectId::new(&subject).map_err(|e| napi::Error::from_reason(e.to_string()))?; + let resource_id = + ResourceId::new(&resource).map_err(|e| napi::Error::from_reason(e.to_string()))?; let cm = parse_consistency(consistency)?; let result = self .engine @@ -418,10 +419,10 @@ impl JsAegis { ) -> napi::Result { self.check_open()?; catch_engine_panic(|| { - let subject_id = SubjectId::new(&subject) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; - let resource_id = ResourceId::new(&resource) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; + let subject_id = + SubjectId::new(&subject).map_err(|e| napi::Error::from_reason(e.to_string()))?; + let resource_id = + ResourceId::new(&resource).map_err(|e| napi::Error::from_reason(e.to_string()))?; let cm = parse_consistency(consistency)?; let ctx = ConditionEvalContext { subject_meta: context.subject_meta.unwrap_or_default(), @@ -450,10 +451,10 @@ impl JsAegis { ) -> napi::Result { self.check_open()?; catch_engine_panic(|| { - let subject_id = SubjectId::new(&subject) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; - let resource_id = ResourceId::new(&resource) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; + let subject_id = + SubjectId::new(&subject).map_err(|e| napi::Error::from_reason(e.to_string()))?; + let resource_id = + ResourceId::new(&resource).map_err(|e| napi::Error::from_reason(e.to_string()))?; let cm = parse_consistency(consistency)?; let ctx = ConditionEvalContext { subject_meta: context.subject_meta.unwrap_or_default(), @@ -483,17 +484,19 @@ impl JsAegis { ) -> napi::Result { self.check_open()?; catch_engine_panic(|| { - let subject_id = SubjectId::new(&subject) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; - let relation_id = Relation::new(&relation) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; - let object_id = ResourceId::new(&resource) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; + let subject_id = + SubjectId::new(&subject).map_err(|e| napi::Error::from_reason(e.to_string()))?; + let relation_id = + Relation::new(&relation).map_err(|e| napi::Error::from_reason(e.to_string()))?; + let object_id = + ResourceId::new(&resource).map_err(|e| napi::Error::from_reason(e.to_string()))?; let valid_until_dt = match valid_until { Some(ref s) => Some( DateTime::parse_from_rfc3339(s) - .map_err(|e| napi::Error::from_reason(format!("invalid valid_until: {}", e)))? + .map_err(|e| { + napi::Error::from_reason(format!("invalid valid_until: {}", e)) + })? .with_timezone(&Utc), ), None => None, @@ -548,8 +551,8 @@ impl JsAegis { ) -> napi::Result> { self.check_open()?; catch_engine_panic(|| { - let object_id = ResourceId::new(&object) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; + let object_id = + ResourceId::new(&object).map_err(|e| napi::Error::from_reason(e.to_string()))?; let relation_opt = relation .as_deref() .map(Relation::new) @@ -574,10 +577,10 @@ impl JsAegis { ) -> napi::Result { self.check_open()?; catch_engine_panic(|| { - let subject_id = SubjectId::new(&subject) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; - let resource_id = ResourceId::new(&resource) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; + let subject_id = + SubjectId::new(&subject).map_err(|e| napi::Error::from_reason(e.to_string()))?; + let resource_id = + ResourceId::new(&resource).map_err(|e| napi::Error::from_reason(e.to_string()))?; let cm = parse_consistency(consistency)?; let result = self .engine @@ -620,10 +623,10 @@ impl JsAegis { ) -> napi::Result { self.check_open()?; catch_engine_panic(|| { - let subject_id = SubjectId::new(&subject) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; - let resource_id = ResourceId::new(&resource) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; + let subject_id = + SubjectId::new(&subject).map_err(|e| napi::Error::from_reason(e.to_string()))?; + let resource_id = + ResourceId::new(&resource).map_err(|e| napi::Error::from_reason(e.to_string()))?; let cm = parse_consistency(consistency)?; let result = self .engine @@ -645,8 +648,8 @@ impl JsAegis { ) -> napi::Result> { self.check_open()?; catch_engine_panic(|| { - let subject_id = SubjectId::new(&subject) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; + let subject_id = + SubjectId::new(&subject).map_err(|e| napi::Error::from_reason(e.to_string()))?; let relation_opt = relation .as_deref() .map(Relation::new) @@ -669,10 +672,10 @@ impl JsAegis { ) -> napi::Result> { self.check_open()?; catch_engine_panic(|| { - let object_id = ResourceId::new(&object) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; - let relation_id = Relation::new(&relation) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; + let object_id = + ResourceId::new(&object).map_err(|e| napi::Error::from_reason(e.to_string()))?; + let relation_id = + Relation::new(&relation).map_err(|e| napi::Error::from_reason(e.to_string()))?; let tuples = self .engine .list_by_relation(&object_id, &relation_id) @@ -700,9 +703,9 @@ impl JsAegis { self.check_open()?; catch_engine_panic(|| { let relation = match filter.relation { - Some(r) => Some( - Relation::new(&r).map_err(|e| napi::Error::from_reason(e.to_string()))?, - ), + Some(r) => { + Some(Relation::new(&r).map_err(|e| napi::Error::from_reason(e.to_string()))?) + } None => None, }; let tf = TupleFilter { @@ -720,12 +723,12 @@ impl JsAegis { .map_err(|e| napi::Error::from_reason(e.to_string()))?; let pp = PaginationParams { limit: pagination.limit as u64, - cursor: pagination.cursor_offset.map(|o| { - aegis_core::types::PaginationCursor { + cursor: pagination + .cursor_offset + .map(|o| aegis_core::types::PaginationCursor { offset: o as u64, revision: current_rev, - } - }), + }), }; let cm = parse_consistency(consistency)?; let result = self @@ -755,7 +758,9 @@ impl JsAegis { let valid_until_dt = match t.valid_until { Some(ref s) => Some( DateTime::parse_from_rfc3339(s) - .map_err(|e| napi::Error::from_reason(format!("invalid valid_until: {}", e)))? + .map_err(|e| { + napi::Error::from_reason(format!("invalid valid_until: {}", e)) + })? .with_timezone(&Utc), ), None => None, @@ -793,8 +798,8 @@ impl JsAegis { pub fn check_schema(&self, schema_yaml: String) -> napi::Result { self.check_open()?; catch_engine_panic(|| { - let new_schema = parse_schema(&schema_yaml) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; + let new_schema = + parse_schema(&schema_yaml).map_err(|e| napi::Error::from_reason(e.to_string()))?; let report = self.engine.check_schema(&new_schema); Ok(SchemaCheckReportNAP { compatible: report.compatible, @@ -808,8 +813,8 @@ impl JsAegis { pub fn delete_object(&self, object: String) -> napi::Result { self.check_open()?; catch_engine_panic(|| { - let object_id = ResourceId::new(&object) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; + let object_id = + ResourceId::new(&object).map_err(|e| napi::Error::from_reason(e.to_string()))?; let result = self .engine .delete_object(&object_id) @@ -836,16 +841,18 @@ impl JsAegis { ) -> napi::Result { self.check_open()?; catch_engine_panic(|| { - let subject_id = SubjectId::new(&subject) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; - let relation_id = Relation::new(&relation) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; - let object_id = ResourceId::new(&resource) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; + let subject_id = + SubjectId::new(&subject).map_err(|e| napi::Error::from_reason(e.to_string()))?; + let relation_id = + Relation::new(&relation).map_err(|e| napi::Error::from_reason(e.to_string()))?; + let object_id = + ResourceId::new(&resource).map_err(|e| napi::Error::from_reason(e.to_string()))?; let valid_until_dt = match valid_until { Some(ref s) => Some( DateTime::parse_from_rfc3339(s) - .map_err(|e| napi::Error::from_reason(format!("invalid valid_until: {}", e)))? + .map_err(|e| { + napi::Error::from_reason(format!("invalid valid_until: {}", e)) + })? .with_timezone(&Utc), ), None => None, @@ -872,14 +879,11 @@ impl JsAegis { // S3.3 — export_subject (GDPR) #[napi] - pub fn export_subject( - &self, - subject: String, - ) -> napi::Result { + pub fn export_subject(&self, subject: String) -> napi::Result { self.check_open()?; catch_engine_panic(|| { - let subject_id = SubjectId::new(&subject) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; + let subject_id = + SubjectId::new(&subject).map_err(|e| napi::Error::from_reason(e.to_string()))?; let tuples = self .engine .export_subject(&subject_id) @@ -910,13 +914,12 @@ impl JsAegis { ) -> napi::Result { self.check_open()?; catch_engine_panic(|| { - let subject_id = SubjectId::new(&subject) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; + let subject_id = + SubjectId::new(&subject).map_err(|e| napi::Error::from_reason(e.to_string()))?; let transfer = match transfer_to_subject { - Some(s) => Some( - SubjectId::new(&s) - .map_err(|e| napi::Error::from_reason(e.to_string()))?, - ), + Some(s) => { + Some(SubjectId::new(&s).map_err(|e| napi::Error::from_reason(e.to_string()))?) + } None => None, }; let result = self @@ -938,12 +941,10 @@ impl JsAegis { ) -> napi::Result> { self.check_open()?; catch_engine_panic(|| { - let object_id = ResourceId::new(&object) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; - let from = from_revision - .map(|r| aegis_core::types::Revision::from(r as u64)); - let to = - to_revision.map(|r| aegis_core::types::Revision::from(r as u64)); + let object_id = + ResourceId::new(&object).map_err(|e| napi::Error::from_reason(e.to_string()))?; + let from = from_revision.map(|r| aegis_core::types::Revision::from(r as u64)); + let to = to_revision.map(|r| aegis_core::types::Revision::from(r as u64)); let pp = PaginationParams { limit: limit as u64, cursor: None, @@ -998,8 +999,8 @@ impl JsAegis { pub fn reload_schema(&self, schema_yaml: String) -> napi::Result<()> { self.check_open()?; catch_engine_panic(|| { - let new_schema = parse_schema(&schema_yaml) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; + let new_schema = + parse_schema(&schema_yaml).map_err(|e| napi::Error::from_reason(e.to_string()))?; self.engine .reload_schema(new_schema) .map_err(|e| napi::Error::from_reason(e.to_string())) @@ -1008,6 +1009,7 @@ impl JsAegis { // S3.10 — set_rate_limiter #[napi] + #[allow(clippy::too_many_arguments)] pub fn set_rate_limiter( &self, checks_per_second: Option, @@ -1021,14 +1023,29 @@ impl JsAegis { self.check_open()?; catch_engine_panic(|| { let mut cfg = RateLimitConfig::default(); - if let Some(v) = checks_per_second { cfg.checks_per_second = v; } - if let Some(v) = check_burst { cfg.check_burst = v; } - if let Some(v) = writes_per_second { cfg.writes_per_second = v; } - if let Some(v) = write_burst { cfg.write_burst = v; } - if let Some(v) = max_traversal_depth { cfg.max_traversal_depth = v as usize; } - if let Some(v) = max_traversal_visits { cfg.max_traversal_visits = v as usize; } - if let Some(v) = max_keys { cfg.max_keys = v as usize; } - self.engine.set_rate_limiter(TokenBucketRateLimiter::new(cfg)); + if let Some(v) = checks_per_second { + cfg.checks_per_second = v; + } + if let Some(v) = check_burst { + cfg.check_burst = v; + } + if let Some(v) = writes_per_second { + cfg.writes_per_second = v; + } + if let Some(v) = write_burst { + cfg.write_burst = v; + } + if let Some(v) = max_traversal_depth { + cfg.max_traversal_depth = v as usize; + } + if let Some(v) = max_traversal_visits { + cfg.max_traversal_visits = v as usize; + } + if let Some(v) = max_keys { + cfg.max_keys = v as usize; + } + self.engine + .set_rate_limiter(TokenBucketRateLimiter::new(cfg)); Ok(()) }) } @@ -1058,19 +1075,20 @@ impl JsAegis { ]) })?; - self.engine.set_logger(move |level: LogLevel, target: &str, msg: &str| { - let level_i32 = match level { - LogLevel::Error => 0, - LogLevel::Warn => 1, - LogLevel::Info => 2, - LogLevel::Debug => 3, - LogLevel::Trace => 4, - }; - let _ = tsfn.call( - (level_i32, target.to_string(), msg.to_string()), - ThreadsafeFunctionCallMode::NonBlocking, - ); - }); + self.engine + .set_logger(move |level: LogLevel, target: &str, msg: &str| { + let level_i32 = match level { + LogLevel::Error => 0, + LogLevel::Warn => 1, + LogLevel::Info => 2, + LogLevel::Debug => 3, + LogLevel::Trace => 4, + }; + let _ = tsfn.call( + (level_i32, target.to_string(), msg.to_string()), + ThreadsafeFunctionCallMode::NonBlocking, + ); + }); Ok(()) }) } @@ -1129,10 +1147,10 @@ impl JsAegis { ) -> napi::Result { self.check_open()?; catch_engine_panic(|| { - let subject_id = SubjectId::new(&subject) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; - let resource_id = ResourceId::new(&resource) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; + let subject_id = + SubjectId::new(&subject).map_err(|e| napi::Error::from_reason(e.to_string()))?; + let resource_id = + ResourceId::new(&resource).map_err(|e| napi::Error::from_reason(e.to_string()))?; let cm = parse_consistency(consistency)?; let result = self .engine @@ -1154,8 +1172,8 @@ impl JsAegis { ) -> napi::Result { self.check_open()?; catch_engine_panic(|| { - let resource_id = ResourceId::new(&resource) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; + let resource_id = + ResourceId::new(&resource).map_err(|e| napi::Error::from_reason(e.to_string()))?; let pagination = PaginationParams { limit: page_limit.unwrap_or(100.0) as u64, cursor: page_offset.map(|o| PaginationCursor { @@ -1194,7 +1212,12 @@ impl JsAegis { .map_err(|e| napi::Error::from_reason(format!("invalid schema_after: {}", e)))?; let result = self .engine - .access_diff(&schema_before, &schema_after, None, max_checks.map(|v| v as u64)) + .access_diff( + &schema_before, + &schema_after, + None, + max_checks.map(|v| v as u64), + ) .map_err(|e| napi::Error::from_reason(e.to_string()))?; serde_json::to_string(&result) .map_err(|e| napi::Error::from_reason(format!("serialization error: {}", e))) @@ -1230,11 +1253,7 @@ impl JsAegis { #[napi] impl JsAegis { #[napi] - pub fn create_policy_draft( - &self, - name: String, - description: String, - ) -> napi::Result { + pub fn create_policy_draft(&self, name: String, description: String) -> napi::Result { self.check_open()?; catch_engine_panic(|| { let draft = self @@ -1247,11 +1266,7 @@ impl JsAegis { } #[napi] - pub fn update_policy_draft( - &self, - id: String, - schema_json: String, - ) -> napi::Result { + pub fn update_policy_draft(&self, id: String, schema_json: String) -> napi::Result { self.check_open()?; catch_engine_panic(|| { let uid = uuid::Uuid::parse_str(&id) @@ -1313,7 +1328,11 @@ impl JsAegis { } #[napi] - pub fn reject_policy_draft(&self, id: String, rejection_reason: String) -> napi::Result { + pub fn reject_policy_draft( + &self, + id: String, + rejection_reason: String, + ) -> napi::Result { self.check_open()?; catch_engine_panic(|| { let uid = uuid::Uuid::parse_str(&id) @@ -1365,7 +1384,9 @@ impl JsAegis { .as_deref() .map(|s| match s.to_lowercase().as_str() { "drafting" => Ok(aegis_core::engine::policy_lifecycle::DraftStatus::Drafting), - "underreview" => Ok(aegis_core::engine::policy_lifecycle::DraftStatus::UnderReview), + "underreview" => { + Ok(aegis_core::engine::policy_lifecycle::DraftStatus::UnderReview) + } "approved" => Ok(aegis_core::engine::policy_lifecycle::DraftStatus::Approved), "rejected" => Ok(aegis_core::engine::policy_lifecycle::DraftStatus::Rejected), "published" => Ok(aegis_core::engine::policy_lifecycle::DraftStatus::Published), @@ -1394,8 +1415,9 @@ impl JsAegis { ) -> napi::Result { self.check_open()?; catch_engine_panic(|| { - let queries: Vec = serde_json::from_str(&queries_json) - .map_err(|e| napi::Error::from_reason(format!("invalid queries: {}", e)))?; + let queries: Vec = + serde_json::from_str(&queries_json) + .map_err(|e| napi::Error::from_reason(format!("invalid queries: {}", e)))?; let compare_schema = match compare_schema_json { Some(s) => Some( serde_json::from_str(&s) @@ -1442,7 +1464,10 @@ impl JsAegis { self.check_open()?; catch_engine_panic(|| { let uid = schedule_id - .map(|id| uuid::Uuid::parse_str(&id).map_err(|e| napi::Error::from_reason(format!("invalid id: {}", e)))) + .map(|id| { + uuid::Uuid::parse_str(&id) + .map_err(|e| napi::Error::from_reason(format!("invalid id: {}", e))) + }) .transpose()?; let runs = self .engine @@ -1523,7 +1548,10 @@ impl JsAegis { "integrityfinding" => Ok(WatchEventType::IntegrityFinding), "analysiscompleted" => Ok(WatchEventType::AnalysisCompleted), "ratelimitwarning" => Ok(WatchEventType::RateLimitWarning), - _ => Err(napi::Error::from_reason(format!("unknown event type: {}", s))), + _ => Err(napi::Error::from_reason(format!( + "unknown event type: {}", + s + ))), }) .collect::>>()?; let subscription = self.engine.subscribe(types); diff --git a/crates/aegis-pyo3/src/lib.rs b/crates/aegis-pyo3/src/lib.rs index 0240c92..958258e 100644 --- a/crates/aegis-pyo3/src/lib.rs +++ b/crates/aegis-pyo3/src/lib.rs @@ -2,15 +2,15 @@ use std::collections::HashMap; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; +use aegis_core::engine::GraphEngine; use aegis_core::engine::condition::ConditionEvalContext; use aegis_core::engine::hooks::LogLevel; use aegis_core::engine::ratelimit::{RateLimitConfig, TokenBucketRateLimiter}; -use aegis_core::engine::GraphEngine; use aegis_core::schema::parse_schema; -use aegis_core::storage::sqlite::{SqliteConfig, SqliteStorage}; use aegis_core::storage::StorageBackend; -use aegis_core::types::*; +use aegis_core::storage::sqlite::{SqliteConfig, SqliteStorage}; use aegis_core::types::PartitionId; +use aegis_core::types::*; use chrono::{DateTime, Utc}; use pyo3::exceptions::PyRuntimeError; @@ -24,11 +24,16 @@ fn py_err(msg: impl ToString) -> PyErr { fn parse_consistency(s: Option) -> PyResult> { match s { None => Ok(None), - Some(ref val) if val.eq_ignore_ascii_case("minimize_latency") => Ok(Some(ConsistencyMode::MinimizeLatency)), - Some(ref val) if val.eq_ignore_ascii_case("fully_consistent") => Ok(Some(ConsistencyMode::FullyConsistent)), + Some(ref val) if val.eq_ignore_ascii_case("minimize_latency") => { + Ok(Some(ConsistencyMode::MinimizeLatency)) + } + Some(ref val) if val.eq_ignore_ascii_case("fully_consistent") => { + Ok(Some(ConsistencyMode::FullyConsistent)) + } Some(ref val) => { if let Some(rev_str) = val.strip_prefix("at_revision:") { - let rev_num: u64 = rev_str.parse() + let rev_num: u64 = rev_str + .parse() .map_err(|_| py_err(format!("invalid consistency: {}", val)))?; Ok(Some(ConsistencyMode::AtRevision(Revision::from(rev_num)))) } else { @@ -52,7 +57,10 @@ struct PyCheckResult { #[pymethods] impl PyCheckResult { fn __repr__(&self) -> String { - format!("CheckResult(allowed={}, revision={})", self.allowed, self.revision) + format!( + "CheckResult(allowed={}, revision={})", + self.allowed, self.revision + ) } } @@ -110,8 +118,10 @@ struct PyHealthReport { #[pymethods] impl PyHealthReport { fn __repr__(&self) -> String { - format!("HealthReport(healthy={}, revision={}, schema_version={})", - self.healthy, self.revision, self.schema_version) + format!( + "HealthReport(healthy={}, revision={}, schema_version={})", + self.healthy, self.revision, self.schema_version + ) } } @@ -129,7 +139,10 @@ struct PyExplainTrace { #[pymethods] impl PyExplainTrace { fn __repr__(&self) -> String { - format!("ExplainTrace({} {} {})", self.subject, self.relation, self.object) + format!( + "ExplainTrace({} {} {})", + self.subject, self.relation, self.object + ) } } @@ -151,8 +164,10 @@ struct PyExplainResult { #[pymethods] impl PyExplainResult { fn __repr__(&self) -> String { - format!("ExplainResult(allowed={}, revision={}, resolved_via={})", - self.allowed, self.revision, self.resolved_via) + format!( + "ExplainResult(allowed={}, revision={}, resolved_via={})", + self.allowed, self.revision, self.resolved_via + ) } } @@ -170,7 +185,10 @@ struct PyTuple { #[pymethods] impl PyTuple { fn __repr__(&self) -> String { - format!("Tuple({} --{}--> {})", self.subject, self.relation, self.object) + format!( + "Tuple({} --{}--> {})", + self.subject, self.relation, self.object + ) } } @@ -188,8 +206,12 @@ struct PySchemaCheckReport { #[pymethods] impl PySchemaCheckReport { fn __repr__(&self) -> String { - format!("SchemaCheckReport(compatible={}, {} warnings, {} breaking)", - self.compatible, self.warnings.len(), self.breaking.len()) + format!( + "SchemaCheckReport(compatible={}, {} warnings, {} breaking)", + self.compatible, + self.warnings.len(), + self.breaking.len() + ) } } @@ -209,7 +231,11 @@ struct PyExportResult { #[pymethods] impl PyExportResult { fn __repr__(&self) -> String { - format!("ExportResult(subject={}, {} tuples)", self.subject, self.active_tuples.len()) + format!( + "ExportResult(subject={}, {} tuples)", + self.subject, + self.active_tuples.len() + ) } } @@ -235,7 +261,10 @@ struct PyAuditEntry { #[pymethods] impl PyAuditEntry { fn __repr__(&self) -> String { - format!("AuditEntry(revision={}, action={})", self.revision, self.action) + format!( + "AuditEntry(revision={}, action={})", + self.revision, self.action + ) } } @@ -253,7 +282,11 @@ struct PyPaginatedTuples { #[pymethods] impl PyPaginatedTuples { fn __repr__(&self) -> String { - format!("PaginatedTuples({} tuples, revision={})", self.tuples.len(), self.revision) + format!( + "PaginatedTuples({} tuples, revision={})", + self.tuples.len(), + self.revision + ) } } @@ -279,7 +312,14 @@ struct PyAegis { impl PyAegis { #[new] #[pyo3(signature = (path, schema_yaml, max_readers=None, busy_timeout_ms=None, wal_mode=None, mmap_size=None))] - fn new(path: String, schema_yaml: String, max_readers: Option, busy_timeout_ms: Option, wal_mode: Option, mmap_size: Option) -> PyResult { + fn new( + path: String, + schema_yaml: String, + max_readers: Option, + busy_timeout_ms: Option, + wal_mode: Option, + mmap_size: Option, + ) -> PyResult { let config = SqliteConfig { path, max_readers: max_readers.unwrap_or(4), @@ -322,14 +362,22 @@ impl PyAegis { } #[pyo3(signature = (subject, permission, resource, consistency=None))] - fn check(&self, subject: &str, permission: &str, resource: &str, consistency: Option) -> PyResult { + fn check( + &self, + subject: &str, + permission: &str, + resource: &str, + consistency: Option, + ) -> PyResult { if self.closed.load(Ordering::Relaxed) { return Err(py_err("engine is closed")); } let subject_id = SubjectId::new(subject).map_err(py_err)?; let resource_id = ResourceId::new(resource).map_err(py_err)?; let cm = parse_consistency(consistency)?; - let result = self.engine.check(&subject_id, permission, &resource_id, cm) + let result = self + .engine + .check(&subject_id, permission, &resource_id, cm) .map_err(py_err)?; Ok(PyCheckResult { allowed: result.allowed, @@ -338,7 +386,14 @@ impl PyAegis { } #[pyo3(signature = (subject, permission, resource, context, consistency=None))] - fn check_with_context(&self, subject: &str, permission: &str, resource: &str, context: HashMap>, consistency: Option) -> PyResult { + fn check_with_context( + &self, + subject: &str, + permission: &str, + resource: &str, + context: HashMap>, + consistency: Option, + ) -> PyResult { if self.closed.load(Ordering::Relaxed) { return Err(py_err("engine is closed")); } @@ -350,7 +405,9 @@ impl PyAegis { resource_meta: context.get("resource_meta").cloned().unwrap_or_default(), env: context.get("env").cloned().unwrap_or_default(), }; - let result = self.engine.check_with_context(&subject_id, permission, &resource_id, cm, ctx) + let result = self + .engine + .check_with_context(&subject_id, permission, &resource_id, cm, ctx) .map_err(py_err)?; Ok(PyCheckResult { allowed: result.allowed, @@ -359,14 +416,22 @@ impl PyAegis { } #[pyo3(signature = (subject, permission, resource, consistency=None))] - fn check_dry_run(&self, subject: &str, permission: &str, resource: &str, consistency: Option) -> PyResult { + fn check_dry_run( + &self, + subject: &str, + permission: &str, + resource: &str, + consistency: Option, + ) -> PyResult { if self.closed.load(Ordering::Relaxed) { return Err(py_err("engine is closed")); } let subject_id = SubjectId::new(subject).map_err(py_err)?; let resource_id = ResourceId::new(resource).map_err(py_err)?; let cm = parse_consistency(consistency)?; - let result = self.engine.check_dry_run(&subject_id, permission, &resource_id, cm) + let result = self + .engine + .check_dry_run(&subject_id, permission, &resource_id, cm) .map_err(py_err)?; Ok(PyCheckResult { allowed: result.allowed, @@ -375,7 +440,15 @@ impl PyAegis { } #[pyo3(signature = (subject, relation, resource, condition=None, metadata=None, valid_until=None))] - fn write(&self, subject: &str, relation: &str, resource: &str, condition: Option, metadata: Option>, valid_until: Option) -> PyResult { + fn write( + &self, + subject: &str, + relation: &str, + resource: &str, + condition: Option, + metadata: Option>, + valid_until: Option, + ) -> PyResult { if self.closed.load(Ordering::Relaxed) { return Err(py_err("engine is closed")); } @@ -403,12 +476,23 @@ impl PyAegis { Ok(PyWriteResult { revision: result.revision.as_u64() as i64, node_id: result.node_id.to_string(), - timestamp: result.timestamp.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(), + timestamp: result + .timestamp + .format("%Y-%m-%dT%H:%M:%S%.3fZ") + .to_string(), }) } #[pyo3(signature = (subject, relation, resource, condition=None, metadata=None, valid_until=None))] - fn write_dry_run(&self, subject: &str, relation: &str, resource: &str, condition: Option, metadata: Option>, valid_until: Option) -> PyResult { + fn write_dry_run( + &self, + subject: &str, + relation: &str, + resource: &str, + condition: Option, + metadata: Option>, + valid_until: Option, + ) -> PyResult { if self.closed.load(Ordering::Relaxed) { return Err(py_err("engine is closed")); } @@ -455,7 +539,10 @@ impl PyAegis { Ok(PyWriteResult { revision: result.revision.as_u64() as i64, node_id: result.node_id.to_string(), - timestamp: result.timestamp.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(), + timestamp: result + .timestamp + .format("%Y-%m-%dT%H:%M:%S%.3fZ") + .to_string(), }) } @@ -480,50 +567,84 @@ impl PyAegis { } #[pyo3(signature = (subject, permission, resource, consistency=None))] - fn explain(&self, subject: &str, permission: &str, resource: &str, consistency: Option) -> PyResult { + fn explain( + &self, + subject: &str, + permission: &str, + resource: &str, + consistency: Option, + ) -> PyResult { if self.closed.load(Ordering::Relaxed) { return Err(py_err("engine is closed")); } let subject_id = SubjectId::new(subject).map_err(py_err)?; let resource_id = ResourceId::new(resource).map_err(py_err)?; let cm = parse_consistency(consistency)?; - let result = self.engine.explain(&subject_id, permission, &resource_id, cm) + let result = self + .engine + .explain(&subject_id, permission, &resource_id, cm) .map_err(py_err)?; Ok(PyExplainResult { allowed: result.allowed, revision: result.revision.as_u64() as i64, - trace: result.trace.iter().map(|t| PyExplainTrace { - subject: t.subject.clone(), - relation: t.relation.clone(), - object: t.object.clone(), - }).collect(), + trace: result + .trace + .iter() + .map(|t| PyExplainTrace { + subject: t.subject.clone(), + relation: t.relation.clone(), + object: t.object.clone(), + }) + .collect(), resolved_via: result.resolved_via, duration_ms: result.duration_ms as i64, }) } #[pyo3(signature = (object, relation=None, consistency=None))] - fn list_by_object(&self, object: &str, relation: Option, consistency: Option) -> PyResult> { + fn list_by_object( + &self, + object: &str, + relation: Option, + consistency: Option, + ) -> PyResult> { if self.closed.load(Ordering::Relaxed) { return Err(py_err("engine is closed")); } let object_id = ResourceId::new(object).map_err(py_err)?; - let rel = relation.as_deref().map(Relation::new).transpose().map_err(py_err)?; + let rel = relation + .as_deref() + .map(Relation::new) + .transpose() + .map_err(py_err)?; let cm = parse_consistency(consistency)?; - let tuples = self.engine.list_by_object(&object_id, rel.as_ref(), cm) + let tuples = self + .engine + .list_by_object(&object_id, rel.as_ref(), cm) .map_err(py_err)?; Ok(tuples.iter().map(tuple_to_py).collect()) } #[pyo3(signature = (subject, relation=None, consistency=None))] - fn list_by_subject(&self, subject: &str, relation: Option, consistency: Option) -> PyResult> { + fn list_by_subject( + &self, + subject: &str, + relation: Option, + consistency: Option, + ) -> PyResult> { if self.closed.load(Ordering::Relaxed) { return Err(py_err("engine is closed")); } let subject_id = SubjectId::new(subject).map_err(py_err)?; - let rel = relation.as_deref().map(Relation::new).transpose().map_err(py_err)?; + let rel = relation + .as_deref() + .map(Relation::new) + .transpose() + .map_err(py_err)?; let cm = parse_consistency(consistency)?; - let tuples = self.engine.list_by_subject(&subject_id, rel.as_ref(), cm) + let tuples = self + .engine + .list_by_subject(&subject_id, rel.as_ref(), cm) .map_err(py_err)?; Ok(tuples.iter().map(tuple_to_py).collect()) } @@ -534,21 +655,38 @@ impl PyAegis { } let object_id = ResourceId::new(object).map_err(py_err)?; let relation_id = Relation::new(relation).map_err(py_err)?; - let tuples = self.engine.list_by_relation(&object_id, &relation_id) + let tuples = self + .engine + .list_by_relation(&object_id, &relation_id) .map_err(py_err)?; Ok(tuples.iter().map(tuple_to_py).collect()) } #[pyo3(signature = (permission, resource, page_offset=None, page_limit=None, include_paths=None))] - fn who_can_access(&self, permission: &str, resource: &str, page_offset: Option, page_limit: Option, include_paths: Option) -> PyResult { + fn who_can_access( + &self, + permission: &str, + resource: &str, + page_offset: Option, + page_limit: Option, + include_paths: Option, + ) -> PyResult { if self.closed.load(Ordering::Relaxed) { return Err(py_err("engine is closed")); } let resource_id = ResourceId::new(resource).map_err(py_err)?; - let cursor = page_offset.map(|offset| PaginationCursor { offset, revision: Revision::from(0) }); - let pagination = PaginationParams { limit: page_limit.unwrap_or(100), cursor }; + let cursor = page_offset.map(|offset| PaginationCursor { + offset, + revision: Revision::from(0), + }); + let pagination = PaginationParams { + limit: page_limit.unwrap_or(100), + cursor, + }; let include = include_paths.unwrap_or(false); - let result = self.engine.who_can_access(permission, &resource_id, &pagination, include, 10, 5000) + let result = self + .engine + .who_can_access(permission, &resource_id, &pagination, include, 10, 5000) .map_err(py_err)?; serde_json::to_string(&result).map_err(py_err) } @@ -569,7 +707,10 @@ impl PyAegis { Ok(PyWriteResult { revision: result.revision.as_u64() as i64, node_id: result.node_id.to_string(), - timestamp: result.timestamp.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(), + timestamp: result + .timestamp + .format("%Y-%m-%dT%H:%M:%S%.3fZ") + .to_string(), }) } @@ -603,7 +744,10 @@ impl PyAegis { Ok(PyWriteResult { revision: result.revision.as_u64() as i64, node_id: result.node_id.to_string(), - timestamp: result.timestamp.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(), + timestamp: result + .timestamp + .format("%Y-%m-%dT%H:%M:%S%.3fZ") + .to_string(), }) } @@ -613,7 +757,11 @@ impl PyAegis { } let subject_id = SubjectId::new(&subject).map_err(py_err)?; let tuples = self.engine.export_subject(&subject_id).map_err(py_err)?; - let revision = self.engine.storage().current_revision(&PartitionId::default()).map_err(py_err)?; + let revision = self + .engine + .storage() + .current_revision(&PartitionId::default()) + .map_err(py_err)?; Ok(PyExportResult { subject: subject.clone(), active_tuples: tuples.iter().map(tuple_to_py).collect(), @@ -623,7 +771,12 @@ impl PyAegis { } #[pyo3(signature = (subject, policy, transfer_to_subject=None))] - fn delete_subject_with_policy(&self, subject: String, policy: String, transfer_to_subject: Option) -> PyResult { + fn delete_subject_with_policy( + &self, + subject: String, + policy: String, + transfer_to_subject: Option, + ) -> PyResult { if self.closed.load(Ordering::Relaxed) { return Err(py_err("engine is closed")); } @@ -632,54 +785,85 @@ impl PyAegis { Some(s) => Some(SubjectId::new(&s).map_err(py_err)?), None => None, }; - let result = self.engine.delete_subject_with_policy(&subject_id, &policy, transfer.as_ref()) + let result = self + .engine + .delete_subject_with_policy(&subject_id, &policy, transfer.as_ref()) .map_err(py_err)?; Ok(PyWriteResult { revision: result.revision.as_u64() as i64, node_id: result.node_id.to_string(), - timestamp: result.timestamp.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(), + timestamp: result + .timestamp + .format("%Y-%m-%dT%H:%M:%S%.3fZ") + .to_string(), }) } #[pyo3(signature = (object, from_revision=None, to_revision=None, limit=100.0))] - fn query_audit(&self, object: String, from_revision: Option, to_revision: Option, limit: f64) -> PyResult> { + fn query_audit( + &self, + object: String, + from_revision: Option, + to_revision: Option, + limit: f64, + ) -> PyResult> { if self.closed.load(Ordering::Relaxed) { return Err(py_err("engine is closed")); } let object_id = ResourceId::new(&object).map_err(py_err)?; let from = from_revision.map(|r| Revision::from(r as u64)); let to = to_revision.map(|r| Revision::from(r as u64)); - let pp = PaginationParams { limit: limit as u64, cursor: None }; - let entries = self.engine.query_audit(&object_id, from, to, &pp).map_err(py_err)?; - Ok(entries.iter().map(|e| PyAuditEntry { - revision: e.revision.as_u64() as i64, - action: format!("{:?}", e.action).to_lowercase(), - subject: e.subject.clone(), - relation: e.relation.clone(), - object: e.object.clone(), - timestamp: e.timestamp.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(), - identity: e.identity.clone(), - }).collect()) + let pp = PaginationParams { + limit: limit as u64, + cursor: None, + }; + let entries = self + .engine + .query_audit(&object_id, from, to, &pp) + .map_err(py_err)?; + Ok(entries + .iter() + .map(|e| PyAuditEntry { + revision: e.revision.as_u64() as i64, + action: format!("{:?}", e.action).to_lowercase(), + subject: e.subject.clone(), + relation: e.relation.clone(), + object: e.object.clone(), + timestamp: e.timestamp.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(), + identity: e.identity.clone(), + }) + .collect()) } #[pyo3(signature = (from_revision=None, to_revision=None, limit=100.0))] - fn query_audit_all(&self, from_revision: Option, to_revision: Option, limit: f64) -> PyResult> { + fn query_audit_all( + &self, + from_revision: Option, + to_revision: Option, + limit: f64, + ) -> PyResult> { if self.closed.load(Ordering::Relaxed) { return Err(py_err("engine is closed")); } let from = from_revision.map(|r| Revision::from(r as u64)); let to = to_revision.map(|r| Revision::from(r as u64)); - let pp = PaginationParams { limit: limit as u64, cursor: None }; + let pp = PaginationParams { + limit: limit as u64, + cursor: None, + }; let entries = self.engine.query_audit_all(from, to, &pp).map_err(py_err)?; - Ok(entries.iter().map(|e| PyAuditEntry { - revision: e.revision.as_u64() as i64, - action: format!("{:?}", e.action).to_lowercase(), - subject: e.subject.clone(), - relation: e.relation.clone(), - object: e.object.clone(), - timestamp: e.timestamp.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(), - identity: e.identity.clone(), - }).collect()) + Ok(entries + .iter() + .map(|e| PyAuditEntry { + revision: e.revision.as_u64() as i64, + action: format!("{:?}", e.action).to_lowercase(), + subject: e.subject.clone(), + relation: e.relation.clone(), + object: e.object.clone(), + timestamp: e.timestamp.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(), + identity: e.identity.clone(), + }) + .collect()) } fn reload_schema(&self, schema_yaml: String) -> PyResult<()> { @@ -700,19 +884,44 @@ impl PyAegis { } #[pyo3(signature = (checks_per_second=None, check_burst=None, writes_per_second=None, write_burst=None, max_traversal_depth=None, max_traversal_visits=None, max_keys=None))] - fn set_rate_limiter(&self, checks_per_second: Option, check_burst: Option, writes_per_second: Option, write_burst: Option, max_traversal_depth: Option, max_traversal_visits: Option, max_keys: Option) -> PyResult<()> { + #[allow(clippy::too_many_arguments)] + fn set_rate_limiter( + &self, + checks_per_second: Option, + check_burst: Option, + writes_per_second: Option, + write_burst: Option, + max_traversal_depth: Option, + max_traversal_visits: Option, + max_keys: Option, + ) -> PyResult<()> { if self.closed.load(Ordering::Relaxed) { return Err(py_err("engine is closed")); } let mut cfg = RateLimitConfig::default(); - if let Some(v) = checks_per_second { cfg.checks_per_second = v; } - if let Some(v) = check_burst { cfg.check_burst = v; } - if let Some(v) = writes_per_second { cfg.writes_per_second = v; } - if let Some(v) = write_burst { cfg.write_burst = v; } - if let Some(v) = max_traversal_depth { cfg.max_traversal_depth = v; } - if let Some(v) = max_traversal_visits { cfg.max_traversal_visits = v; } - if let Some(v) = max_keys { cfg.max_keys = v; } - self.engine.set_rate_limiter(TokenBucketRateLimiter::new(cfg)); + if let Some(v) = checks_per_second { + cfg.checks_per_second = v; + } + if let Some(v) = check_burst { + cfg.check_burst = v; + } + if let Some(v) = writes_per_second { + cfg.writes_per_second = v; + } + if let Some(v) = write_burst { + cfg.write_burst = v; + } + if let Some(v) = max_traversal_depth { + cfg.max_traversal_depth = v; + } + if let Some(v) = max_traversal_visits { + cfg.max_traversal_visits = v; + } + if let Some(v) = max_keys { + cfg.max_keys = v; + } + self.engine + .set_rate_limiter(TokenBucketRateLimiter::new(cfg)); Ok(()) } @@ -729,30 +938,39 @@ impl PyAegis { if self.closed.load(Ordering::Relaxed) { return Err(py_err("engine is closed")); } - self.engine.set_logger(move |level: LogLevel, target: &str, msg: &str| { - let level_i32 = match level { - LogLevel::Error => 0, - LogLevel::Warn => 1, - LogLevel::Info => 2, - LogLevel::Debug => 3, - LogLevel::Trace => 4, - }; - Python::with_gil(|py| { - let _ = callback.call1(py, (level_i32, target, msg)); + self.engine + .set_logger(move |level: LogLevel, target: &str, msg: &str| { + let level_i32 = match level { + LogLevel::Error => 0, + LogLevel::Warn => 1, + LogLevel::Info => 2, + LogLevel::Debug => 3, + LogLevel::Trace => 4, + }; + Python::with_gil(|py| { + let _ = callback.call1(py, (level_i32, target, msg)); + }); }); - }); Ok(()) } #[pyo3(signature = (subject, permission, resource, consistency=None))] - fn explain_v2(&self, subject: &str, permission: &str, resource: &str, consistency: Option) -> PyResult { + fn explain_v2( + &self, + subject: &str, + permission: &str, + resource: &str, + consistency: Option, + ) -> PyResult { if self.closed.load(Ordering::Relaxed) { return Err(py_err("engine is closed")); } let subject_id = SubjectId::new(subject).map_err(py_err)?; let resource_id = ResourceId::new(resource).map_err(py_err)?; let cm = parse_consistency(consistency)?; - let result = self.engine.explain_v2(&subject_id, permission, &resource_id, cm) + let result = self + .engine + .explain_v2(&subject_id, permission, &resource_id, cm) .map_err(py_err)?; serde_json::to_string(&result).map_err(py_err) } @@ -774,13 +992,20 @@ impl PyAegis { } #[pyo3(signature = (schema_before_json, schema_after_json, max_checks=None))] - fn access_diff(&self, schema_before_json: &str, schema_after_json: &str, max_checks: Option) -> PyResult { + fn access_diff( + &self, + schema_before_json: &str, + schema_after_json: &str, + max_checks: Option, + ) -> PyResult { if self.closed.load(Ordering::Relaxed) { return Err(py_err("engine is closed")); } let schema_before: Schema = serde_json::from_str(schema_before_json).map_err(py_err)?; let schema_after: Schema = serde_json::from_str(schema_after_json).map_err(py_err)?; - let result = self.engine.access_diff(&schema_before, &schema_after, None, max_checks) + let result = self + .engine + .access_diff(&schema_before, &schema_after, None, max_checks) .map_err(py_err)?; serde_json::to_string(&result).map_err(py_err) } @@ -791,7 +1016,10 @@ impl PyAegis { if self.closed.load(Ordering::Relaxed) { return Err(py_err("engine is closed")); } - let draft = self.engine.create_policy_draft(name, description).map_err(py_err)?; + let draft = self + .engine + .create_policy_draft(name, description) + .map_err(py_err)?; serde_json::to_string(&draft).map_err(py_err) } @@ -801,7 +1029,10 @@ impl PyAegis { } let uid = uuid::Uuid::parse_str(id).map_err(py_err)?; let schema: Schema = serde_json::from_str(schema_json).map_err(py_err)?; - let draft = self.engine.update_policy_draft(uid, schema).map_err(py_err)?; + let draft = self + .engine + .update_policy_draft(uid, schema) + .map_err(py_err)?; serde_json::to_string(&draft).map_err(py_err) } @@ -819,7 +1050,10 @@ impl PyAegis { return Err(py_err("engine is closed")); } let uid = uuid::Uuid::parse_str(id).map_err(py_err)?; - let draft = self.engine.submit_policy_draft_for_review(uid).map_err(py_err)?; + let draft = self + .engine + .submit_policy_draft_for_review(uid) + .map_err(py_err)?; serde_json::to_string(&draft).map_err(py_err) } @@ -837,7 +1071,10 @@ impl PyAegis { return Err(py_err("engine is closed")); } let uid = uuid::Uuid::parse_str(id).map_err(py_err)?; - let draft = self.engine.reject_policy_draft(uid, rejection_reason).map_err(py_err)?; + let draft = self + .engine + .reject_policy_draft(uid, rejection_reason) + .map_err(py_err)?; serde_json::to_string(&draft).map_err(py_err) } @@ -882,15 +1119,24 @@ impl PyAegis { // ── V7 Scheduler ── #[pyo3(signature = (name, interval_seconds, queries_json, compare_schema_json=None))] - fn create_analysis_schedule(&self, name: &str, interval_seconds: f64, queries_json: &str, compare_schema_json: Option<&str>) -> PyResult { + fn create_analysis_schedule( + &self, + name: &str, + interval_seconds: f64, + queries_json: &str, + compare_schema_json: Option<&str>, + ) -> PyResult { if self.closed.load(Ordering::Relaxed) { return Err(py_err("engine is closed")); } - let queries: Vec = serde_json::from_str(queries_json).map_err(py_err)?; + let queries: Vec = + serde_json::from_str(queries_json).map_err(py_err)?; let compare_schema = compare_schema_json .map(|s| serde_json::from_str(s).map_err(py_err)) .transpose()?; - let schedule = self.engine.create_analysis_schedule(name, interval_seconds as u64, queries, compare_schema) + let schedule = self + .engine + .create_analysis_schedule(name, interval_seconds as u64, queries, compare_schema) .map_err(py_err)?; serde_json::to_string(&schedule).map_err(py_err) } @@ -928,7 +1174,10 @@ impl PyAegis { if self.closed.load(Ordering::Relaxed) { return Err(py_err("engine is closed")); } - let runs = self.engine.get_analysis_runs(limit as usize).map_err(py_err)?; + let runs = self + .engine + .get_analysis_runs(limit as usize) + .map_err(py_err)?; serde_json::to_string(&runs).map_err(py_err) } @@ -940,14 +1189,19 @@ impl PyAegis { } let config: aegis_core::engine::enforcement_history::EnforcementHistoryConfig = serde_json::from_str(config_json).map_err(py_err)?; - self.engine.set_enforcement_history_config(config).map_err(py_err) + self.engine + .set_enforcement_history_config(config) + .map_err(py_err) } fn get_enforcement_history_config(&self) -> PyResult { if self.closed.load(Ordering::Relaxed) { return Err(py_err("engine is closed")); } - let config = self.engine.get_enforcement_history_config().map_err(py_err)?; + let config = self + .engine + .get_enforcement_history_config() + .map_err(py_err)?; serde_json::to_string(&config).map_err(py_err) } @@ -956,7 +1210,10 @@ impl PyAegis { if self.closed.load(Ordering::Relaxed) { return Err(py_err("engine is closed")); } - let trends = self.engine.enforcement_trends(limit as usize).map_err(py_err)?; + let trends = self + .engine + .enforcement_trends(limit as usize) + .map_err(py_err)?; serde_json::to_string(&trends).map_err(py_err) } @@ -997,7 +1254,12 @@ impl PyAegis { } #[pyo3(signature = (_exc_type=None, _exc_val=None, _exc_tb=None))] - fn __exit__(&self, _exc_type: Option, _exc_val: Option, _exc_tb: Option) -> PyResult<()> { + fn __exit__( + &self, + _exc_type: Option, + _exc_val: Option, + _exc_tb: Option, + ) -> PyResult<()> { let _ = self.close(); Ok(()) } diff --git a/examples/wasm-demo/Cargo.lock b/examples/wasm-demo/Cargo.lock new file mode 100644 index 0000000..39c18be --- /dev/null +++ b/examples/wasm-demo/Cargo.lock @@ -0,0 +1,1044 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aegis-core" +version = "0.1.0" +dependencies = [ + "chrono", + "ed25519-dalek", + "getrandom 0.2.17", + "hex", + "libc", + "rand", + "serde", + "serde_json", + "serde_yml", + "sha2", + "subtle", + "thiserror", + "tracing", + "uuid", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cc" +version = "1.2.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "signature", + "subtle", + "zeroize", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "log" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "noyalib" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e493c05128df7a83b9676b709d590e0ebc285c7ed3152bc679668e8c1e506af5" +dependencies = [ + "indexmap", + "memchr", + "rustc-hash", + "serde", + "smallvec", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_yml" +version = "0.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909764a65f86829ccdb5eea9ab355843aa02c019a7bfd47465092953565caa05" +dependencies = [ + "noyalib", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "uuid" +version = "1.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-demo" +version = "0.1.0" +dependencies = [ + "aegis-core", + "wasm-bindgen", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/packages/aegis-browser/rust/Cargo.lock b/packages/aegis-browser/rust/Cargo.lock new file mode 100644 index 0000000..f9fb510 --- /dev/null +++ b/packages/aegis-browser/rust/Cargo.lock @@ -0,0 +1,1085 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aegis-browser" +version = "0.1.0" +dependencies = [ + "aegis-core", + "js-sys", + "serde", + "serde_json", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", +] + +[[package]] +name = "aegis-core" +version = "0.1.0" +dependencies = [ + "async-trait", + "chrono", + "ed25519-dalek", + "getrandom 0.2.17", + "hex", + "js-sys", + "libc", + "rand", + "serde", + "serde_json", + "serde_yml", + "sha2", + "subtle", + "thiserror", + "tracing", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cc" +version = "1.2.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "signature", + "subtle", + "zeroize", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "log" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "noyalib" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e493c05128df7a83b9676b709d590e0ebc285c7ed3152bc679668e8c1e506af5" +dependencies = [ + "indexmap", + "memchr", + "rustc-hash", + "serde", + "smallvec", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_yml" +version = "0.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909764a65f86829ccdb5eea9ab355843aa02c019a7bfd47465092953565caa05" +dependencies = [ + "noyalib", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "uuid" +version = "1.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 2210b89..5e19866 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "stable" +channel = "1.86.0" targets = ["x86_64-pc-windows-msvc", "x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"]