summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormo khan <mo@mokhan.ca>2025-07-02 12:32:27 -0600
committermo khan <mo@mokhan.ca>2025-07-02 12:32:27 -0600
commita577c62277e3d651b66fd68dbe800bf3ab5c4921 (patch)
tree7ae4e79fc84c539c12fb0313d0d3cc929b2e12ae
parentc2b8edab01b23fde6cc196a3349ad6aa19a93299 (diff)
parent0b610d061e45811130d8cf3919037fdc9513e340 (diff)
Merge branch 'rs' into 'main'
Re-write the authorization daemon in rust See merge request gitlab-org/software-supply-chain-security/authorization/authzd!1
-rw-r--r--.dockerignore10
-rw-r--r--.gitignore3
-rw-r--r--.gitlab-ci.yml4
-rw-r--r--.runway/env-production.yml2
-rw-r--r--.runway/env-staging.yml2
-rw-r--r--.runway/runway.yml12
-rw-r--r--.tool-versions5
-rw-r--r--Cargo.lock2417
-rw-r--r--Cargo.toml29
-rw-r--r--Dockerfile20
-rw-r--r--Makefile53
-rw-r--r--README.md28
-rw-r--r--app/app.go31
-rw-r--r--app/app_test.go52
-rw-r--r--app/init.go15
-rw-r--r--app/server.go24
-rw-r--r--app/server_test.go63
-rw-r--r--app/services/ability.go27
-rw-r--r--app/services/check.go72
-rw-r--r--app/services/check_test.go95
-rw-r--r--cmd/authzd/main.go16
-rw-r--r--etc/authzd/policy0.cedar20
-rw-r--r--go.mod48
-rw-r--r--go.sum102
-rw-r--r--magefile.go55
-rw-r--r--mise.toml5
-rw-r--r--pkg/.keep0
-rw-r--r--pkg/gid/gid.go36
-rw-r--r--pkg/gid/gid_test.go38
-rw-r--r--pkg/policies/allowed.go29
-rw-r--r--pkg/policies/allowed_test.go146
-rw-r--r--pkg/policies/entities.json286
-rw-r--r--pkg/policies/gtwy.cedar12
-rw-r--r--pkg/policies/init.go60
-rw-r--r--pkg/policies/organization.cedar5
-rw-r--r--pkg/rpc/ability.pb.go194
-rw-r--r--pkg/rpc/ability.twirp.go1104
-rw-r--r--protos/ability.proto19
-rw-r--r--src/authorization/authorizer.rs5
-rw-r--r--src/authorization/cedar_authorizer.rs141
-rw-r--r--src/authorization/check_service.rs35
-rw-r--r--src/authorization/mod.rs9
-rw-r--r--src/authorization/server.rs50
-rw-r--r--src/lib.rs2
-rw-r--r--src/main.rs14
-rw-r--r--tests/authorization/cedar_authorizer_test.rs66
-rw-r--r--tests/authorization/check_service_test.rs118
-rw-r--r--tests/authorization/mod.rs3
-rw-r--r--tests/authorization/server_test.rs48
-rw-r--r--tests/integration_test.rs2
-rw-r--r--tests/support/factory_bot.rs58
-rw-r--r--tests/support/mod.rs1
-rw-r--r--tmp/cache/.keep0
-rw-r--r--tmp/pids/.keep0
54 files changed, 3107 insertions, 2584 deletions
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 00000000..e8da7cb4
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,10 @@
+target/
+.git/
+.gitignore
+*.md
+tests/
+.env*
+Dockerfile*
+.dockerignore
+tags
+mise.toml
diff --git a/.gitignore b/.gitignore
index 1afff66a..eb5a316c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1 @@
-tmp
-*.pdf
+target
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 8d6d4ef3..25648846 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -32,7 +32,7 @@ build image:
- docker push $CONTAINER_IMAGE_COMMIT
unit:
- image: golang:alpine
+ image: rust:latest
stage: test
script:
- - go test ./...
+ - cargo test
diff --git a/.runway/env-production.yml b/.runway/env-production.yml
index 6eaecdb8..9d86e025 100644
--- a/.runway/env-production.yml
+++ b/.runway/env-production.yml
@@ -1,2 +1,2 @@
APP_ENV: "production"
-BIND_ADDR: ":http"
+BIND_ADDR: "[::1]:50051"
diff --git a/.runway/env-staging.yml b/.runway/env-staging.yml
index 6eaecdb8..9d86e025 100644
--- a/.runway/env-staging.yml
+++ b/.runway/env-staging.yml
@@ -1,2 +1,2 @@
APP_ENV: "production"
-BIND_ADDR: ":http"
+BIND_ADDR: "[::1]:50051"
diff --git a/.runway/runway.yml b/.runway/runway.yml
index 43be4694..78f3d526 100644
--- a/.runway/runway.yml
+++ b/.runway/runway.yml
@@ -7,18 +7,15 @@ metadata:
owner_email_handle: mkhan
product_category: authorization
spec:
- container_port: 80
+ container_port: 50051
deployment:
strategy: "expedited"
regions:
- us-east1
startup_probe:
- path: "/health"
- timeout_seconds: 5
- period_seconds: 5
- failure_threshold: 24 # 2 minutes
+ grpc_service: "grpc.health.v1.Health/Check"
liveness_probe:
- path: "/health"
+ grpc_service: "grpc.health.v1.Health/Check"
scalability:
min_instances: 1
max_instances: 1
@@ -29,4 +26,5 @@ spec:
memory: 512Mi
load_balancing:
external_load_balancer:
- backend_protocol: HTTPS
+ enabled: true
+ backend_protocol: HTTP2
diff --git a/.tool-versions b/.tool-versions
deleted file mode 100644
index eb4df2a3..00000000
--- a/.tool-versions
+++ /dev/null
@@ -1,5 +0,0 @@
-cargo latest
-golang 1.24.0
-make 4.4.1
-protoc 3.19.6
-rust stable
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 00000000..4c657df8
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,2417 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "addr2line"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "android-tzdata"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+
+[[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.98"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
+
+[[package]]
+name = "arrayvec"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
+
+[[package]]
+name = "ascii-canvas"
+version = "4.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef1e3e699d84ab1b0911a1010c5c106aa34ae89aeac103be5ce0c3859db1e891"
+dependencies = [
+ "term",
+]
+
+[[package]]
+name = "async-stream"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
+dependencies = [
+ "async-stream-impl",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-stream-impl"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "async-trait"
+version = "0.1.88"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
+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 = "authzd"
+version = "0.1.0"
+dependencies = [
+ "cedar-policy",
+ "envoy-types",
+ "log",
+ "please",
+ "tokio",
+ "tokio-stream",
+ "tokio-test",
+ "tonic",
+ "tonic-build",
+ "tonic-health",
+ "tonic-reflection",
+ "tracing",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "axum"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5"
+dependencies = [
+ "axum-core",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "itoa",
+ "matchit",
+ "memchr",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustversion",
+ "serde",
+ "sync_wrapper",
+ "tower",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "axum-core"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "rustversion",
+ "sync_wrapper",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "backtrace"
+version = "0.3.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
+dependencies = [
+ "addr2line",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "beef"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1"
+
+[[package]]
+name = "bit-set"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
+dependencies = [
+ "bit-vec",
+]
+
+[[package]]
+name = "bit-vec"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
+
+[[package]]
+name = "bitflags"
+version = "2.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
+
+[[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 = "borsh"
+version = "1.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce"
+dependencies = [
+ "cfg_aliases",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
+
+[[package]]
+name = "bytes"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
+
+[[package]]
+name = "cc"
+version = "1.2.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc"
+dependencies = [
+ "shlex",
+]
+
+[[package]]
+name = "cedar-policy"
+version = "4.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1d37df3afde553492d8413d02a733e86a786c29f5b36c6d8d72bc6e5c0c7f63"
+dependencies = [
+ "cedar-policy-core",
+ "cedar-policy-formatter",
+ "cedar-policy-validator",
+ "itertools 0.14.0",
+ "lalrpop-util",
+ "lazy_static",
+ "miette",
+ "nonempty",
+ "ref-cast",
+ "semver",
+ "serde",
+ "serde_json",
+ "serde_with",
+ "smol_str",
+ "thiserror",
+]
+
+[[package]]
+name = "cedar-policy-core"
+version = "4.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd896075d493e8283e960c94cf85d85773dc3359cf948a5f5ea88a6f249e8665"
+dependencies = [
+ "chrono",
+ "educe",
+ "either",
+ "itertools 0.14.0",
+ "lalrpop",
+ "lalrpop-util",
+ "lazy_static",
+ "miette",
+ "nonempty",
+ "ref-cast",
+ "regex",
+ "rustc_lexer",
+ "serde",
+ "serde_json",
+ "serde_with",
+ "smol_str",
+ "stacker",
+ "thiserror",
+]
+
+[[package]]
+name = "cedar-policy-formatter"
+version = "4.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95db50a024d5ed3efe4b9b513124b564313716ada017860c17995ec227ab46b1"
+dependencies = [
+ "cedar-policy-core",
+ "itertools 0.14.0",
+ "lazy_static",
+ "logos",
+ "miette",
+ "pretty",
+ "regex",
+ "smol_str",
+]
+
+[[package]]
+name = "cedar-policy-validator"
+version = "4.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f40762e669e4ec5a4b3a2b3d3aa2567d768aa74b735760f13197312c1150f69"
+dependencies = [
+ "cedar-policy-core",
+ "educe",
+ "itertools 0.14.0",
+ "lalrpop",
+ "lalrpop-util",
+ "lazy_static",
+ "miette",
+ "nonempty",
+ "ref-cast",
+ "serde",
+ "serde_json",
+ "serde_with",
+ "smol_str",
+ "stacker",
+ "thiserror",
+ "unicode-security",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "chrono"
+version = "0.4.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
+dependencies = [
+ "android-tzdata",
+ "iana-time-zone",
+ "num-traits",
+ "serde",
+ "windows-link",
+]
+
+[[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.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[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 = "deranged"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
+dependencies = [
+ "powerfmt",
+ "serde",
+]
+
+[[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 = "dyn-clone"
+version = "1.0.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005"
+
+[[package]]
+name = "educe"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417"
+dependencies = [
+ "enum-ordinalize",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
+[[package]]
+name = "ena"
+version = "0.14.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d248bdd43ce613d87415282f69b9bb99d947d290b10962dd6c56233312c2ad5"
+dependencies = [
+ "log",
+]
+
+[[package]]
+name = "enum-ordinalize"
+version = "4.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fea0dcfa4e54eeb516fe454635a95753ddd39acda650ce703031c6973e315dd5"
+dependencies = [
+ "enum-ordinalize-derive",
+]
+
+[[package]]
+name = "enum-ordinalize-derive"
+version = "4.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "envoy-types"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "065b6b0018b25902cab074d44c0e2098205329b6b5a309a33cc688bc0ac9573d"
+dependencies = [
+ "futures-core",
+ "prost 0.13.5",
+ "tonic",
+]
+
+[[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.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
+dependencies = [
+ "libc",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+[[package]]
+name = "fixedbitset"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
+
+[[package]]
+name = "fixedbitset"
+version = "0.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "futures-channel"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
+
+[[package]]
+name = "futures-sink"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
+
+[[package]]
+name = "futures-task"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
+
+[[package]]
+name = "futures-util"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "pin-project-lite",
+ "pin-utils",
+]
+
+[[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.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasi 0.14.2+wasi-0.2.4",
+]
+
+[[package]]
+name = "gimli"
+version = "0.31.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
+
+[[package]]
+name = "h2"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http",
+ "indexmap 2.9.0",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "hashbrown"
+version = "0.15.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
+
+[[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 = "home"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "http"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
+dependencies = [
+ "bytes",
+ "fnv",
+ "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 = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "hyper"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "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.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "libc",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.63"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
+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 = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+dependencies = [
+ "autocfg",
+ "hashbrown 0.12.3",
+ "serde",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.15.4",
+ "serde",
+]
+
+[[package]]
+name = "itertools"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itertools"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
+[[package]]
+name = "js-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "keccak"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654"
+dependencies = [
+ "cpufeatures",
+]
+
+[[package]]
+name = "lalrpop"
+version = "0.22.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba4ebbd48ce411c1d10fb35185f5a51a7bfa3d8b24b4e330d30c9e3a34129501"
+dependencies = [
+ "ascii-canvas",
+ "bit-set",
+ "ena",
+ "itertools 0.14.0",
+ "lalrpop-util",
+ "petgraph 0.7.1",
+ "pico-args",
+ "regex",
+ "regex-syntax",
+ "sha3",
+ "string_cache",
+ "term",
+ "unicode-xid",
+ "walkdir",
+]
+
+[[package]]
+name = "lalrpop-util"
+version = "0.22.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5baa5e9ff84f1aefd264e6869907646538a52147a755d494517a8007fb48733"
+dependencies = [
+ "regex-automata",
+ "rustversion",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libc"
+version = "0.2.174"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
+
+[[package]]
+name = "lock_api"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
+
+[[package]]
+name = "logos"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab6f536c1af4c7cc81edf73da1f8029896e7e1e16a219ef09b184e76a296f3db"
+dependencies = [
+ "logos-derive",
+]
+
+[[package]]
+name = "logos-codegen"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "189bbfd0b61330abea797e5e9276408f2edbe4f822d7ad08685d67419aafb34e"
+dependencies = [
+ "beef",
+ "fnv",
+ "lazy_static",
+ "proc-macro2",
+ "quote",
+ "regex-syntax",
+ "rustc_version",
+ "syn",
+]
+
+[[package]]
+name = "logos-derive"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebfe8e1a19049ddbfccbd14ac834b215e11b85b90bab0c2dba7c7b92fb5d5cba"
+dependencies = [
+ "logos-codegen",
+]
+
+[[package]]
+name = "matchit"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
+
+[[package]]
+name = "memchr"
+version = "2.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
+
+[[package]]
+name = "miette"
+version = "7.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7"
+dependencies = [
+ "cfg-if",
+ "miette-derive",
+ "serde",
+ "unicode-width",
+]
+
+[[package]]
+name = "miette-derive"
+version = "7.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+]
+
+[[package]]
+name = "mio"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
+dependencies = [
+ "libc",
+ "wasi 0.11.1+wasi-snapshot-preview1",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "multimap"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
+
+[[package]]
+name = "new_debug_unreachable"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
+
+[[package]]
+name = "nonempty"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "303e8749c804ccd6ca3b428de7fe0d86cb86bc7606bc15291f100fd487960bb8"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
+dependencies = [
+ "overload",
+ "winapi",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "object"
+version = "0.36.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "overload"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
+[[package]]
+name = "petgraph"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db"
+dependencies = [
+ "fixedbitset 0.4.2",
+ "indexmap 2.9.0",
+]
+
+[[package]]
+name = "petgraph"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772"
+dependencies = [
+ "fixedbitset 0.5.7",
+ "indexmap 2.9.0",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "pico-args"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
+
+[[package]]
+name = "pin-project"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "please"
+version = "0.1.0"
+source = "git+https://github.com/xlgmokha/please.git#bff13caf9ee2f806dd32c5d77f0e4ac9eb28c7f5"
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "precomputed-hash"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
+
+[[package]]
+name = "pretty"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac98773b7109bc75f475ab5a134c9b64b87e59d776d31098d8f346922396a477"
+dependencies = [
+ "arrayvec",
+ "typed-arena",
+ "unicode-width",
+]
+
+[[package]]
+name = "prettyplease"
+version = "0.2.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.95"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "prost"
+version = "0.12.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29"
+dependencies = [
+ "bytes",
+ "prost-derive 0.12.6",
+]
+
+[[package]]
+name = "prost"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
+dependencies = [
+ "bytes",
+ "prost-derive 0.13.5",
+]
+
+[[package]]
+name = "prost-build"
+version = "0.12.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4"
+dependencies = [
+ "bytes",
+ "heck",
+ "itertools 0.12.1",
+ "log",
+ "multimap",
+ "once_cell",
+ "petgraph 0.6.5",
+ "prettyplease",
+ "prost 0.12.6",
+ "prost-types 0.12.6",
+ "regex",
+ "syn",
+ "tempfile",
+]
+
+[[package]]
+name = "prost-derive"
+version = "0.12.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1"
+dependencies = [
+ "anyhow",
+ "itertools 0.12.1",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "prost-derive"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
+dependencies = [
+ "anyhow",
+ "itertools 0.14.0",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "prost-types"
+version = "0.12.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0"
+dependencies = [
+ "prost 0.12.6",
+]
+
+[[package]]
+name = "prost-types"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16"
+dependencies = [
+ "prost 0.13.5",
+]
+
+[[package]]
+name = "psm"
+version = "0.1.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e944464ec8536cd1beb0bbfd96987eb5e3b72f2ecdafdc5c769a37f1fa2ae1f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+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 = "redox_syscall"
+version = "0.5.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "ref-cast"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf"
+dependencies = [
+ "ref-cast-impl",
+]
+
+[[package]]
+name = "ref-cast-impl"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "regex"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f"
+
+[[package]]
+name = "rustc_lexer"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c86aae0c77166108c01305ee1a36a1e77289d7dc6ca0a3cd91ff4992de2d16a5"
+dependencies = [
+ "unicode-xid",
+]
+
+[[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.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
+
+[[package]]
+name = "ryu"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+[[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 = "schemars"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
+dependencies = [
+ "dyn-clone",
+ "ref-cast",
+ "serde",
+ "serde_json",
+]
+
+[[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.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
+
+[[package]]
+name = "serde"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.140"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
+dependencies = [
+ "indexmap 2.9.0",
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_with"
+version = "3.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf65a400f8f66fb7b0552869ad70157166676db75ed8181f8104ea91cf9d0b42"
+dependencies = [
+ "base64",
+ "chrono",
+ "hex",
+ "indexmap 1.9.3",
+ "indexmap 2.9.0",
+ "schemars",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "serde_with_macros",
+ "time",
+]
+
+[[package]]
+name = "serde_with_macros"
+version = "3.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81679d9ed988d5e9a5e6531dc3f2c28efbd639cbd1dfb628df08edea6004da77"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "sha3"
+version = "0.10.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60"
+dependencies = [
+ "digest",
+ "keccak",
+]
+
+[[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 = "siphasher"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
+
+[[package]]
+name = "slab"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d"
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "smol_str"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9676b89cd56310a87b93dec47b11af744f34d5fc9f367b829474eec0a891350d"
+dependencies = [
+ "borsh",
+ "serde",
+]
+
+[[package]]
+name = "socket2"
+version = "0.5.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "stacker"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cddb07e32ddb770749da91081d8d0ac3a16f1a569a18b20348cd371f5dead06b"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "libc",
+ "psm",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "string_cache"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f"
+dependencies = [
+ "new_debug_unreachable",
+ "parking_lot",
+ "phf_shared",
+ "precomputed-hash",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "syn"
+version = "2.0.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
+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 = "tempfile"
+version = "3.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
+dependencies = [
+ "fastrand",
+ "getrandom",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "term"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a984c8d058c627faaf5e8e2ed493fa3c51771889196de1016cf9c1c6e90d750"
+dependencies = [
+ "home",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
+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 = "time"
+version = "0.3.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
+dependencies = [
+ "deranged",
+ "itoa",
+ "num-conv",
+ "powerfmt",
+ "serde",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
+
+[[package]]
+name = "time-macros"
+version = "0.2.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71"
+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.45.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "libc",
+ "mio",
+ "pin-project-lite",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+ "tokio-util",
+]
+
+[[package]]
+name = "tokio-test"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7"
+dependencies = [
+ "async-stream",
+ "bytes",
+ "futures-core",
+ "tokio",
+ "tokio-stream",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tonic"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e581ba15a835f4d9ea06c55ab1bd4dce26fc53752c69a04aac00703bfb49ba9"
+dependencies = [
+ "async-trait",
+ "axum",
+ "base64",
+ "bytes",
+ "h2",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-timeout",
+ "hyper-util",
+ "percent-encoding",
+ "pin-project",
+ "prost 0.13.5",
+ "socket2",
+ "tokio",
+ "tokio-stream",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tonic-build"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d021fc044c18582b9a2408cd0dd05b1596e3ecdb5c4df822bb0183545683889"
+dependencies = [
+ "prettyplease",
+ "proc-macro2",
+ "prost-build",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tonic-health"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb87334d340313fefa513b6e60794d44a86d5f039b523229c99c323e4e19ca4b"
+dependencies = [
+ "prost 0.13.5",
+ "tokio",
+ "tokio-stream",
+ "tonic",
+]
+
+[[package]]
+name = "tonic-reflection"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9687bd5bfeafebdded2356950f278bba8226f0b32109537c4253406e09aafe1"
+dependencies = [
+ "prost 0.13.5",
+ "prost-types 0.13.5",
+ "tokio",
+ "tokio-stream",
+ "tonic",
+]
+
+[[package]]
+name = "tower"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "indexmap 2.9.0",
+ "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.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
+dependencies = [
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
+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-serde"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1"
+dependencies = [
+ "serde",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
+dependencies = [
+ "nu-ansi-term",
+ "serde",
+ "serde_json",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing-core",
+ "tracing-log",
+ "tracing-serde",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "typed-arena"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
+
+[[package]]
+name = "typenum"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-script"
+version = "0.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fb421b350c9aff471779e262955939f565ec18b86c15364e6bdf0d662ca7c1f"
+
+[[package]]
+name = "unicode-security"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e4ddba1535dd35ed8b61c52166b7155d7f4e4b8847cec6f48e71dc66d8b5e50"
+dependencies = [
+ "unicode-normalization",
+ "unicode-script",
+]
+
+[[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 = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+[[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.2+wasi-0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
+dependencies = [
+ "wit-bindgen-rt",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
+dependencies = [
+ "bumpalo",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-core"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
+
+[[package]]
+name = "windows-result"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
+dependencies = [
+ "windows-link",
+]
+
+[[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.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets 0.53.2",
+]
+
+[[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 0.52.6",
+ "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-targets"
+version = "0.53.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef"
+dependencies = [
+ "windows_aarch64_gnullvm 0.53.0",
+ "windows_aarch64_msvc 0.53.0",
+ "windows_i686_gnu 0.53.0",
+ "windows_i686_gnullvm 0.53.0",
+ "windows_i686_msvc 0.53.0",
+ "windows_x86_64_gnu 0.53.0",
+ "windows_x86_64_gnullvm 0.53.0",
+ "windows_x86_64_msvc 0.53.0",
+]
+
+[[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_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
+
+[[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_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
+
+[[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_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
+
+[[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_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
+
+[[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_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
+
+[[package]]
+name = "wit-bindgen-rt"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
+dependencies = [
+ "bitflags",
+]
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 00000000..ebacce3b
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,29 @@
+[package]
+name = "authzd"
+version = "0.1.0"
+edition = "2024"
+
+[[bin]]
+name = "authzd"
+path = "src/main.rs"
+
+[lib]
+name = "authzd"
+path = "src/lib.rs"
+
+[dependencies]
+cedar-policy = "4.4.1"
+envoy-types = "0.6.0"
+log = "0.4.27"
+please = { git = "https://github.com/xlgmokha/please.git", version = "0.1.0" }
+tokio = { version = "1.0.0", features = ["macros", "rt-multi-thread"] }
+tonic = "0.13.1"
+tonic-health = "0.13.1"
+tonic-reflection = "0.13.1"
+tracing = "0.1"
+tracing-subscriber = { version = "0.3", features = ["json"] }
+
+[dev-dependencies]
+tokio-stream = "0.1"
+tokio-test = "0.4.0"
+tonic-build = "0.10.0"
diff --git a/Dockerfile b/Dockerfile
index d4e2b99d..744b4f3d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,14 +1,14 @@
# syntax=docker/dockerfile:1
-FROM golang:1.24.0 AS build
-ENV CGO_ENABLED=0
+FROM rust:alpine AS builder
+RUN apk add --no-cache musl-dev
WORKDIR /app
COPY . ./
-RUN go build -o authzd ./cmd/authzd/main.go && mv ./authzd /bin/authzd
+RUN cargo build --release --target x86_64-unknown-linux-musl
+RUN strip /app/target/x86_64-unknown-linux-musl/release/authzd
-
-FROM scratch
-ENV BIND_ADDR=":http"
-EXPOSE 80
-WORKDIR /var/www/
-COPY --from=build /bin/authzd /bin/authzd
-CMD ["/bin/authzd"]
+FROM gcr.io/distroless/static-debian12:nonroot
+EXPOSE 50051
+WORKDIR /var/www
+COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/authzd /bin/authzd
+COPY --from=builder /app/etc/authzd /etc/authzd
+ENTRYPOINT ["/bin/authzd"]
diff --git a/Makefile b/Makefile
index 028279a6..a134ae64 100644
--- a/Makefile
+++ b/Makefile
@@ -1,11 +1,42 @@
-default: install-tools
- @mage -l
-
-install-tools:
- @cargo install --keep-going cedar-policy-cli
- @go install tool
- @command -v cedar
- @command -v mage
- @command -v protoc-gen-go
- @command -v protoc-gen-go-grpc
- @command -v protoc-gen-twirp_ruby
+PROJECT_NAME := $(shell basename $(shell pwd))
+GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD | sed 's/\//_/g')
+IMAGE_TAG := $(PROJECT_NAME):$(GIT_BRANCH)
+
+.PHONY: build check test run clean fmt lint doc
+.PHONY: build-image run-image health-check list-services test-grpc
+
+# Cargo targets
+build:
+ @cargo build
+
+check:
+ @cargo check
+
+test:
+ @cargo test
+
+run:
+ @cargo run
+
+clean:
+ @cargo clean
+
+fmt:
+ @cargo fmt
+
+lint:
+ @cargo clippy -- -D warnings
+
+doc:
+ @cargo doc --open
+
+# Docker targets
+build-image:
+ @docker build --no-cache --tag $(IMAGE_TAG) .
+
+run-image: build-image
+ @docker run --rm -p 50051:50051 -it $(IMAGE_TAG)
+
+# gRPC testing targets
+health-check:
+ @grpcurl -plaintext localhost:50051 grpc.health.v1.Health/Check
diff --git a/README.md b/README.md
index 6b414a9f..5d023217 100644
--- a/README.md
+++ b/README.md
@@ -52,28 +52,24 @@ It integrates with an identity provider (IdP) and uses message queues to stay in
```sh
$ mise install
- $ make install-tools
```
1. Start servers:
```sh
- $ mage servers
+ $ cargo run
```
-## Questions
-
-See the [FAQ][9]
-
## References
-* [go tool][5]
-* [gRPC][6]
-* [protocol buffers][7]
-* [twirp][8]
-
-[1]: https://github.com/twitchtv/twirp
-[5]: https://tip.golang.org/doc/modules/managing-dependencies#tools
-[6]: https://grpc.io/docs/
-[7]: https://protobuf.dev/programming-guides/proto3/
-[8]: https://github.com/arthurnn/twirp-ruby/wiki/Code-Generation
+* [gRPC][3]
+* [protocol buffers][4]
+* [tokio][2]
+* [tonic][1]
+* [Envoy External Authorization][5]
+
+[1]: https://github.com/hyperium/tonic
+[2]: https://github.com/tokio-rs/tokio
+[3]: https://grpc.io/docs/
+[4]: https://protobuf.dev/programming-guides/proto3/
+[5]: https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/ext_authz_filter
diff --git a/app/app.go b/app/app.go
deleted file mode 100644
index f79b67b1..00000000
--- a/app/app.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package app
-
-import (
- "net/http"
-
- "github.com/rs/zerolog"
- "github.com/xlgmokha/x/pkg/ioc"
- "github.com/xlgmokha/x/pkg/log"
- "gitlab.com/gitlab-org/software-supply-chain-security/authorization/authzd.git/app/services"
- "gitlab.com/gitlab-org/software-supply-chain-security/authorization/authzd.git/pkg/rpc"
-)
-
-func New() http.Handler {
- mux := http.NewServeMux()
- for _, handler := range handlers() {
- mux.Handle(handler.PathPrefix(), handler)
- }
-
- mux.Handle("/health", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.WriteHeader(http.StatusOK)
- }))
-
- logger := ioc.MustResolve[*zerolog.Logger](ioc.Default)
- return log.HTTP(logger)(mux)
-}
-
-func handlers() []rpc.TwirpServer {
- return []rpc.TwirpServer{
- rpc.NewAbilityServer(services.NewAbilityService()),
- }
-}
diff --git a/app/app_test.go b/app/app_test.go
deleted file mode 100644
index f0068e87..00000000
--- a/app/app_test.go
+++ /dev/null
@@ -1,52 +0,0 @@
-package app
-
-import (
- http "net/http"
- "net/http/httptest"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- "gitlab.com/gitlab-org/software-supply-chain-security/authorization/authzd.git/pkg/rpc"
-)
-
-func TestApp(t *testing.T) {
- handler := New()
- srv := httptest.NewServer(handler)
- defer srv.Close()
-
- t.Run("Ability.Allowed", func(t *testing.T) {
- client := rpc.NewAbilityProtobufClient(srv.URL, &http.Client{})
-
- t.Run("forbids", func(t *testing.T) {
- reply, err := client.Allowed(t.Context(), &rpc.AllowRequest{
- Subject: "",
- Permission: "",
- Resource: "",
- })
- require.NoError(t, err)
- assert.False(t, reply.Result)
- })
-
- t.Run("allows gid://User/1 read gid://Organization/2", func(t *testing.T) {
- reply, err := client.Allowed(t.Context(), &rpc.AllowRequest{
- Subject: "gid://example/User/1",
- Permission: "read",
- Resource: "gid://example/Organization/2",
- })
- require.NoError(t, err)
- assert.True(t, reply.Result)
- })
- })
-
- t.Run("GET /health", func(t *testing.T) {
- t.Run("returns OK", func(t *testing.T) {
- r := httptest.NewRequest("GET", "/health", nil)
- w := httptest.NewRecorder()
-
- handler.ServeHTTP(w, r)
-
- assert.Equal(t, http.StatusOK, w.Code)
- })
- })
-}
diff --git a/app/init.go b/app/init.go
deleted file mode 100644
index 3c4e757e..00000000
--- a/app/init.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package app
-
-import (
- "os"
-
- "github.com/rs/zerolog"
- "github.com/xlgmokha/x/pkg/ioc"
- "github.com/xlgmokha/x/pkg/log"
-)
-
-func init() {
- ioc.RegisterSingleton[*zerolog.Logger](ioc.Default, func() *zerolog.Logger {
- return log.New(os.Stdout, log.Fields{"app": "authzd"})
- })
-}
diff --git a/app/server.go b/app/server.go
deleted file mode 100644
index 3ce0aadc..00000000
--- a/app/server.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package app
-
-import (
- "context"
-
- auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
- "gitlab.com/gitlab-org/software-supply-chain-security/authorization/authzd.git/app/services"
- "google.golang.org/grpc"
- "google.golang.org/grpc/reflection"
-)
-
-type Server struct {
- *grpc.Server
-}
-
-func NewServer(ctx context.Context, options ...grpc.ServerOption) *Server {
- server := grpc.NewServer(options...)
- auth.RegisterAuthorizationServer(server, services.NewCheckService())
- reflection.Register(server)
-
- return &Server{
- Server: server,
- }
-}
diff --git a/app/server_test.go b/app/server_test.go
deleted file mode 100644
index ff34487a..00000000
--- a/app/server_test.go
+++ /dev/null
@@ -1,63 +0,0 @@
-package app
-
-import (
- "context"
- "net"
- "testing"
-
- auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- "google.golang.org/grpc"
- "google.golang.org/grpc/codes"
- "google.golang.org/grpc/credentials/insecure"
- "google.golang.org/grpc/test/bufconn"
-)
-
-type HTTPRequest = auth.AttributeContext_HttpRequest
-
-func TestServer(t *testing.T) {
- socket := bufconn.Listen(1024 * 1024)
- srv := NewServer(t.Context())
-
- defer srv.GracefulStop()
- go func() {
- require.NoError(t, srv.Serve(socket))
- }()
-
- connection, err := grpc.DialContext(
- t.Context(),
- "bufnet",
- grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
- return socket.Dial()
- }),
- grpc.WithTransportCredentials(insecure.NewCredentials()),
- )
- require.NoError(t, err)
- defer connection.Close()
-
- client := auth.NewAuthorizationClient(connection)
-
- t.Run("CheckRequest", func(t *testing.T) {
- tt := []struct {
- http *HTTPRequest
- status codes.Code
- }{
- {status: codes.OK, http: &HTTPRequest{Method: "GET", Path: "/"}},
- }
-
- for _, example := range tt {
- t.Run(example.http.Path, func(t *testing.T) {
- response, err := client.Check(t.Context(), &auth.CheckRequest{
- Attributes: &auth.AttributeContext{
- Request: &auth.AttributeContext_Request{
- Http: example.http,
- },
- },
- })
- require.NoError(t, err)
- assert.Equal(t, int32(example.status), response.Status.Code)
- })
- }
- })
-}
diff --git a/app/services/ability.go b/app/services/ability.go
deleted file mode 100644
index f0379513..00000000
--- a/app/services/ability.go
+++ /dev/null
@@ -1,27 +0,0 @@
-package services
-
-import (
- context "context"
-
- "github.com/cedar-policy/cedar-go"
- "gitlab.com/gitlab-org/software-supply-chain-security/authorization/authzd.git/pkg/gid"
- "gitlab.com/gitlab-org/software-supply-chain-security/authorization/authzd.git/pkg/policies"
- "gitlab.com/gitlab-org/software-supply-chain-security/authorization/authzd.git/pkg/rpc"
-)
-
-type AbilityService struct {
-}
-
-func NewAbilityService() *AbilityService {
- return &AbilityService{}
-}
-
-func (h *AbilityService) Allowed(ctx context.Context, req *rpc.AllowRequest) (*rpc.AllowReply, error) {
- ok := policies.Allowed(ctx, cedar.Request{
- Principal: gid.NewEntityUID(req.Subject),
- Action: cedar.NewEntityUID("Permission", cedar.String(req.Permission)),
- Resource: gid.NewEntityUID(req.Resource),
- Context: cedar.NewRecord(cedar.RecordMap{}),
- })
- return &rpc.AllowReply{Result: ok}, nil
-}
diff --git a/app/services/check.go b/app/services/check.go
deleted file mode 100644
index 23deecb9..00000000
--- a/app/services/check.go
+++ /dev/null
@@ -1,72 +0,0 @@
-package services
-
-import (
- "context"
-
- "github.com/cedar-policy/cedar-go"
- core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
- auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
- types "github.com/envoyproxy/go-control-plane/envoy/type/v3"
- "gitlab.com/gitlab-org/software-supply-chain-security/authorization/authzd.git/pkg/gid"
- "gitlab.com/gitlab-org/software-supply-chain-security/authorization/authzd.git/pkg/policies"
- status "google.golang.org/genproto/googleapis/rpc/status"
- "google.golang.org/grpc/codes"
-)
-
-type CheckService struct {
- auth.UnimplementedAuthorizationServer
-}
-
-func NewCheckService() *CheckService {
- return &CheckService{}
-}
-
-func (svc *CheckService) Check(ctx context.Context, request *auth.CheckRequest) (*auth.CheckResponse, error) {
- if svc.isAllowed(ctx, request) {
- return svc.OK(ctx), nil
- }
- return svc.Denied(ctx), nil
-}
-
-func (svc *CheckService) isAllowed(ctx context.Context, r *auth.CheckRequest) bool {
- return policies.Allowed(ctx, cedar.Request{
- Principal: gid.NewEntityUID("gid://gitlab/User/*"),
- Action: cedar.NewEntityUID("HttpMethod", cedar.String(r.Attributes.Request.Http.Method)),
- Resource: cedar.NewEntityUID("HttpPath", cedar.String(r.Attributes.Request.Http.Path)),
- Context: cedar.NewRecord(cedar.RecordMap{
- "host": cedar.String(r.Attributes.Request.Http.Host),
- }),
- })
-
-}
-
-func (svc *CheckService) OK(ctx context.Context) *auth.CheckResponse {
- return &auth.CheckResponse{
- Status: &status.Status{
- Code: int32(codes.OK),
- },
- HttpResponse: &auth.CheckResponse_OkResponse{
- OkResponse: &auth.OkHttpResponse{
- Headers: []*core.HeaderValueOption{},
- HeadersToRemove: []string{},
- ResponseHeadersToAdd: []*core.HeaderValueOption{},
- },
- },
- }
-}
-
-func (svc *CheckService) Denied(ctx context.Context) *auth.CheckResponse {
- return &auth.CheckResponse{
- Status: &status.Status{
- Code: int32(codes.PermissionDenied),
- },
- HttpResponse: &auth.CheckResponse_DeniedResponse{
- DeniedResponse: &auth.DeniedHttpResponse{
- Status: &types.HttpStatus{
- Code: types.StatusCode_Unauthorized,
- },
- Headers: []*core.HeaderValueOption{},
- },
- },
- }
-}
diff --git a/app/services/check_test.go b/app/services/check_test.go
deleted file mode 100644
index 4eb396bb..00000000
--- a/app/services/check_test.go
+++ /dev/null
@@ -1,95 +0,0 @@
-package services
-
-import (
- "strings"
- "testing"
-
- core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
- auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- "google.golang.org/protobuf/types/known/timestamppb"
-)
-
-func TestCheckService(t *testing.T) {
- svc := NewCheckService()
-
- t.Run("allows access", func(t *testing.T) {
- idToken := "header.payload.signature"
- accessToken := "f88f60df11e458b594c80b299aee05f8e5805c65c3e779cc6fbc606c4ac36227"
- refreshToken := "0847d325d6e4f021c4baaae0ddb425dbd8795807a4751cd2131bec8e8a9aee24"
-
- cookies := []string{
- "bearer_token=" + accessToken + ";",
- "id_token=" + idToken + ";",
- "refresh_token=" + refreshToken,
- }
-
- response, err := svc.Check(t.Context(), &auth.CheckRequest{
- Attributes: &auth.AttributeContext{
- Source: &auth.AttributeContext_Peer{
- Address: &core.Address{
- Address: &core.Address_SocketAddress{
- SocketAddress: &core.SocketAddress{
- Address: "127.0.0.1",
- PortSpecifier: &core.SocketAddress_PortValue{
- PortValue: 52358,
- },
- },
- },
- },
- },
- Destination: &auth.AttributeContext_Peer{
- Address: &core.Address{
- Address: &core.Address_SocketAddress{
- SocketAddress: &core.SocketAddress{
- Address: "127.0.0.1",
- PortSpecifier: &core.SocketAddress_PortValue{
- PortValue: 10000,
- },
- },
- },
- },
- },
- Request: &auth.AttributeContext_Request{
- Time: &timestamppb.Timestamp{Seconds: 1747937928, Nanos: 476481000},
- Http: &auth.AttributeContext_HttpRequest{
- Id: "1248474133684962828",
- Method: "GET",
- Headers: map[string]string{
- ":authority": "localhost:10000",
- ":method": "GET",
- ":path": "/health",
- ":scheme": "http",
- "accept": "*/*",
- "accept-encoding": "gzip, deflate, br, zstd",
- "accept-language": "en-US,en;q=0.9",
- "cache-control": "max-age=0",
- "content-length": "64",
- "content-type": "application/json",
- "cookie": strings.Join(cookies, "; "),
- "origin": "http://localhost:10000",
- "referer": "http://localhost:10000/dashboard",
- "sec-ch-ua-mobile": "?0",
- "sec-ch-ua-platform": "Linux",
- "sec-fetch-dest": "empty",
- "sec-fetch-mode": "cors",
- "sec-fetch-site": "same-origin",
- "x-forwarded-proto": "http",
- "x-request-id": "7e064610-9e19-4a38-8354-0de0b5fbd7c6",
- },
- Path: "/health",
- Host: "localhost:10000",
- Scheme: "http",
- Protocol: "HTTP/1.1",
- },
- },
- MetadataContext: &core.Metadata{},
- RouteMetadataContext: &core.Metadata{},
- },
- })
-
- require.NoError(t, err)
- assert.NotNil(t, response.GetOkResponse())
- })
-}
diff --git a/cmd/authzd/main.go b/cmd/authzd/main.go
deleted file mode 100644
index 05b0b148..00000000
--- a/cmd/authzd/main.go
+++ /dev/null
@@ -1,16 +0,0 @@
-package main
-
-import (
- "fmt"
- "log"
- "net/http"
-
- "github.com/xlgmokha/x/pkg/env"
- "gitlab.com/gitlab-org/software-supply-chain-security/authorization/authzd.git/app"
-)
-
-func main() {
- bindAddr := env.Fetch("BIND_ADDR", "localhost:8080")
- fmt.Printf("Listening on %v\n", bindAddr)
- log.Fatal(http.ListenAndServe(bindAddr, app.New()))
-}
diff --git a/etc/authzd/policy0.cedar b/etc/authzd/policy0.cedar
new file mode 100644
index 00000000..034e81b5
--- /dev/null
+++ b/etc/authzd/policy0.cedar
@@ -0,0 +1,20 @@
+permit(principal, action == Action::"check", resource)
+when {
+ context has bearer_token &&
+ context.bearer_token == "valid-token"
+};
+
+permit(principal, action == Action::"check", resource)
+when {
+ context has path && (
+ context.path like "*.css" ||
+ context.path like "*.js" ||
+ context.path like "*.ico" ||
+ context.path like "*.png" ||
+ context.path like "*.jpg" ||
+ context.path like "*.jpeg" ||
+ context.path like "*.gif" ||
+ context.path like "*.bmp" ||
+ context.path like "*.html"
+ )
+};
diff --git a/go.mod b/go.mod
deleted file mode 100644
index 0da45df9..00000000
--- a/go.mod
+++ /dev/null
@@ -1,48 +0,0 @@
-module gitlab.com/gitlab-org/software-supply-chain-security/authorization/authzd.git
-
-go 1.24
-
-require (
- github.com/cedar-policy/cedar-go v1.2.1
- github.com/envoyproxy/go-control-plane/envoy v1.32.4
- github.com/magefile/mage v1.15.0
- github.com/rs/zerolog v1.34.0
- github.com/stretchr/testify v1.10.0
- github.com/twitchtv/twirp v8.1.3+incompatible
- github.com/xlgmokha/x v0.0.0-20250430185455-b691eda27477
- google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4
- google.golang.org/grpc v1.71.0
- google.golang.org/protobuf v1.36.6
-)
-
-require (
- github.com/arthurnn/twirp-ruby v1.13.0 // indirect
- github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3 // indirect
- github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
- github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
- github.com/golang/protobuf v1.5.4 // indirect
- github.com/golobby/container/v3 v3.3.2 // indirect
- github.com/google/go-cmp v0.7.0 // indirect
- github.com/kr/pretty v0.3.1 // indirect
- github.com/mattn/go-colorable v0.1.14 // indirect
- github.com/mattn/go-isatty v0.0.20 // indirect
- github.com/pkg/errors v0.9.1 // indirect
- github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
- github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
- github.com/rogpeppe/go-internal v1.13.1 // indirect
- golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
- golang.org/x/net v0.37.0 // indirect
- golang.org/x/sys v0.32.0 // indirect
- golang.org/x/text v0.24.0 // indirect
- google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect
- gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
- gopkg.in/yaml.v3 v3.0.1 // indirect
-)
-
-tool (
- github.com/arthurnn/twirp-ruby/protoc-gen-twirp_ruby
- github.com/magefile/mage
- github.com/twitchtv/twirp/protoc-gen-twirp
- google.golang.org/grpc/cmd/protoc-gen-go-grpc
- google.golang.org/protobuf/cmd/protoc-gen-go
-)
diff --git a/go.sum b/go.sum
deleted file mode 100644
index 0a713aca..00000000
--- a/go.sum
+++ /dev/null
@@ -1,102 +0,0 @@
-github.com/arthurnn/twirp-ruby v1.13.0 h1:j0T7I5oxe2niKFdfjiiCmkiydwYeegrbwVMs+Gajm6M=
-github.com/arthurnn/twirp-ruby v1.13.0/go.mod h1:1fVOQuSLzwXoPi9/ejlDYG3roilJIPAZN2sw+A3o48o=
-github.com/cedar-policy/cedar-go v1.2.1 h1:f4Ie1j6OT2TqtmnbVcR7OyJWfyPEUvYrFYaurQZhTAo=
-github.com/cedar-policy/cedar-go v1.2.1/go.mod h1:Ahhlu3DxDzvGTR88v/+j/EeiMEyvta11hlkdDgj4AZU=
-github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3 h1:boJj011Hh+874zpIySeApCX4GeOjPl9qhRF3QuIZq+Q=
-github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
-github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
-github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
-github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
-github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=
-github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
-github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
-github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
-github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
-github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
-github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
-github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
-github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
-github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
-github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
-github.com/golobby/container/v3 v3.3.2 h1:7u+RgNnsdVlhGoS8gY4EXAG601vpMMzLZlYqSp77Quw=
-github.com/golobby/container/v3 v3.3.2/go.mod h1:RDdKpnKpV1Of11PFBe7Dxc2C1k2KaLE4FD47FflAmj0=
-github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
-github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
-github.com/google/jsonapi v1.0.0 h1:qIGgO5Smu3yJmSs+QlvhQnrscdZfFhiV6S8ryJAglqU=
-github.com/google/jsonapi v1.0.0/go.mod h1:YYHiRPJT8ARXGER8In9VuLv4qvLfDmA9ULQqptbLE4s=
-github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
-github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
-github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
-github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
-github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
-github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
-github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
-github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
-github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
-github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
-github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
-github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
-github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
-github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
-github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
-github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
-github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
-github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
-github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
-github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
-github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
-github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
-github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
-github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
-github.com/twitchtv/twirp v8.1.3+incompatible h1:+F4TdErPgSUbMZMwp13Q/KgDVuI7HJXP61mNV3/7iuU=
-github.com/twitchtv/twirp v8.1.3+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vFK5YBhA6bqp2l1kfCC5A=
-github.com/xlgmokha/x v0.0.0-20250430185455-b691eda27477 h1:oeKrn9BSDSic5MTXKhQesxhIhB7byxl86IHtCD+yw4k=
-github.com/xlgmokha/x v0.0.0-20250430185455-b691eda27477/go.mod h1:axGPKzoJCNTmPJxYqN5l+Z9gGbPe0yolkT61a5p3QiI=
-go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
-go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
-go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
-go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
-go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
-go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
-go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
-go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
-go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
-go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
-go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
-golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
-golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
-golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
-golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
-golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
-golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
-golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
-golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
-google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
-google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
-google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A=
-google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA=
-google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
-google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
-gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
-gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
-gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
-gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
-gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/magefile.go b/magefile.go
deleted file mode 100644
index 49cd4c87..00000000
--- a/magefile.go
+++ /dev/null
@@ -1,55 +0,0 @@
-//go:build mage
-// +build mage
-
-package main
-
-import (
- "context"
- "path/filepath"
-
- "github.com/magefile/mage/mg"
- "github.com/magefile/mage/sh"
- "github.com/xlgmokha/x/pkg/x"
-)
-
-// Default target to run when none is specified
-// If not set, running mage will list available targets
-var Default = Servers
-
-// Run the Authzd Service
-func Authzd() error {
- env := map[string]string{
- "BIND_ADDR": ":8080",
- }
- return sh.RunWithV(env, "go", "run", "./cmd/authzd/main.go")
-}
-
-// Generate gRPC from protocal buffers
-func Protos() error {
- for _, file := range x.Must(filepath.Glob("./protos/*.proto")) {
- if err := sh.RunV(
- "protoc",
- "--proto_path=./protos",
- "--go_out=.",
- "--twirp_out=.",
- file,
- ); err != nil {
- return err
- }
- }
-
- return nil
-}
-
-// Run All the servers
-func Servers(ctx context.Context) {
- mg.CtxDeps(ctx, Authzd)
-}
-
-// Run the end to end tests
-func Test(ctx context.Context) error {
- mg.CtxDeps(ctx, func() error {
- return sh.RunV("go", "clean", "-testcache")
- })
- return sh.RunV("go", "test", "-shuffle=on", "-v", "./...")
-}
diff --git a/mise.toml b/mise.toml
new file mode 100644
index 00000000..276d2e72
--- /dev/null
+++ b/mise.toml
@@ -0,0 +1,5 @@
+[tools]
+grpcurl = "latest"
+make = "latest"
+rust = "latest"
+rust-analyzer = "latest"
diff --git a/pkg/.keep b/pkg/.keep
deleted file mode 100644
index e69de29b..00000000
--- a/pkg/.keep
+++ /dev/null
diff --git a/pkg/gid/gid.go b/pkg/gid/gid.go
deleted file mode 100644
index 7d1ea978..00000000
--- a/pkg/gid/gid.go
+++ /dev/null
@@ -1,36 +0,0 @@
-package gid
-
-import (
- "net/url"
- "strings"
-
- "github.com/cedar-policy/cedar-go"
-)
-
-func NewEntityUID(globalID string) cedar.EntityUID {
- if !strings.HasPrefix(globalID, "gid://") {
- return DefaultEntityUID(globalID)
- }
-
- url, err := url.Parse(globalID)
- if err != nil {
- return DefaultEntityUID(globalID)
- }
- items := strings.SplitN(url.Path, "/", 3)
- if len(items) != 3 {
- return DefaultEntityUID(globalID)
- }
-
- return cedar.NewEntityUID(
- cedar.EntityType(items[1]),
- cedar.String(items[2]),
- )
-}
-
-func DefaultEntityUID(id string) cedar.EntityUID {
- return cedar.NewEntityUID("User", cedar.String(id))
-}
-
-func ZeroEntityUID() cedar.EntityUID {
- return cedar.NewEntityUID("", "")
-}
diff --git a/pkg/gid/gid_test.go b/pkg/gid/gid_test.go
deleted file mode 100644
index e1f6285b..00000000
--- a/pkg/gid/gid_test.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package gid
-
-import (
- "testing"
-
- "github.com/cedar-policy/cedar-go"
- "github.com/stretchr/testify/assert"
-)
-
-func TestNewEntityUID(t *testing.T) {
- t.Run("returns an Entity UID with an integer id", func(t *testing.T) {
- result := NewEntityUID("gid://example/User/1")
-
- assert.Equal(t, cedar.EntityType("User"), result.Type)
- assert.Equal(t, cedar.String("1"), result.ID)
- })
-
- t.Run("returns an Entity UID with a UUID", func(t *testing.T) {
- result := NewEntityUID("gid://example/User/4707ce42-1017-11f0-acdf-7ec11f4b308c")
-
- assert.Equal(t, cedar.EntityType("User"), result.Type)
- assert.Equal(t, cedar.String("4707ce42-1017-11f0-acdf-7ec11f4b308c"), result.ID)
- })
-
- t.Run("returns an Entity UID with a namespace", func(t *testing.T) {
- result := NewEntityUID("gid://example/Authn::User/1")
-
- assert.Equal(t, cedar.EntityType("Authn::User"), result.Type)
- assert.Equal(t, cedar.String("1"), result.ID)
- })
-
- t.Run("returns a default when a global id is not provided", func(t *testing.T) {
- result := NewEntityUID("alice")
-
- assert.Equal(t, cedar.EntityType("User"), result.Type)
- assert.Equal(t, cedar.String("alice"), result.ID)
- })
-}
diff --git a/pkg/policies/allowed.go b/pkg/policies/allowed.go
deleted file mode 100644
index 733c08b8..00000000
--- a/pkg/policies/allowed.go
+++ /dev/null
@@ -1,29 +0,0 @@
-package policies
-
-import (
- "context"
-
- "github.com/cedar-policy/cedar-go"
- "github.com/cedar-policy/cedar-go/types"
- "github.com/xlgmokha/x/pkg/log"
-)
-
-func Allowed(ctx context.Context, request cedar.Request) bool {
- ok, diagnostic := All.IsAuthorized(Entities, request)
-
- log.WithFields(ctx, log.Fields{
- "ok": ok,
- "principal": request.Principal,
- "action": request.Action,
- "context": request.Context,
- "resource": request.Resource,
- })
-
- if len(diagnostic.Errors) > 0 {
- log.WithFields(ctx, log.Fields{"errors": diagnostic.Errors})
- }
- if len(diagnostic.Reasons) > 0 {
- log.WithFields(ctx, log.Fields{"reasons": diagnostic.Reasons})
- }
- return ok == types.Allow
-}
diff --git a/pkg/policies/allowed_test.go b/pkg/policies/allowed_test.go
deleted file mode 100644
index 367bd99f..00000000
--- a/pkg/policies/allowed_test.go
+++ /dev/null
@@ -1,146 +0,0 @@
-package policies
-
-import (
- "fmt"
- "testing"
-
- "github.com/cedar-policy/cedar-go"
- "github.com/stretchr/testify/assert"
- "gitlab.com/gitlab-org/software-supply-chain-security/authorization/authzd.git/pkg/gid"
-)
-
-func build(f func(*cedar.Request)) *cedar.Request {
- request := &cedar.Request{
- Principal: gid.NewEntityUID("gid://example/User/1"),
- Action: cedar.NewEntityUID("HttpMethod", "GET"),
- Resource: cedar.NewEntityUID("HttpPath", "/"),
- Context: cedar.NewRecord(cedar.RecordMap{
- "host": cedar.String("example.com"),
- }),
- }
- f(request)
- return request
-}
-
-func TestAllowed(t *testing.T) {
- allowed := []*cedar.Request{
- build(func(r *cedar.Request) {}),
- build(func(r *cedar.Request) {
- r.Principal = gid.NewEntityUID("gid://example/User/1")
- r.Action = cedar.NewEntityUID("HttpMethod", "POST")
- }),
- build(func(r *cedar.Request) {
- r.Principal = gid.NewEntityUID("gid://example/User/1")
- r.Action = cedar.NewEntityUID("HttpMethod", "PUT")
- }),
- build(func(r *cedar.Request) {
- r.Principal = gid.NewEntityUID("gid://example/User/1")
- r.Action = cedar.NewEntityUID("HttpMethod", "PATCH")
- }),
- build(func(r *cedar.Request) {
- r.Principal = gid.NewEntityUID("gid://example/User/1")
- r.Action = cedar.NewEntityUID("HttpMethod", "DELETE")
- }),
- build(func(r *cedar.Request) {
- r.Principal = gid.NewEntityUID("gid://example/User/1")
- r.Action = cedar.NewEntityUID("HttpMethod", "HEAD")
- }),
- build(func(r *cedar.Request) {
- r.Principal = gid.NewEntityUID("gid://example/User/1")
- r.Resource = cedar.NewEntityUID("HttpPath", "/organizations.json")
- r.Context = cedar.NewRecord(cedar.RecordMap{
- "host": cedar.String("api.example.com"),
- })
- }),
- build(func(r *cedar.Request) {
- r.Principal = gid.NewEntityUID("gid://example/User/1")
- r.Resource = cedar.NewEntityUID("HttpPath", "/groups.json")
- r.Context = cedar.NewRecord(cedar.RecordMap{
- "host": cedar.String("api.example.com"),
- })
- }),
- build(func(r *cedar.Request) {
- r.Principal = gid.NewEntityUID("gid://example/User/1")
- r.Resource = cedar.NewEntityUID("HttpPath", "/.well-known/openid-configuration")
- r.Context = cedar.NewRecord(cedar.RecordMap{
- "host": cedar.String("idp.example.com"),
- })
- }),
- build(func(r *cedar.Request) {
- r.Principal = gid.NewEntityUID("gid://example/User/1")
- r.Resource = cedar.NewEntityUID("HttpPath", "/.well-known/oauth-authorization-server")
- r.Context = cedar.NewRecord(cedar.RecordMap{
- "host": cedar.String("idp.example.com"),
- })
- }),
- build(func(r *cedar.Request) {
- r.Principal = gid.NewEntityUID("gid://example/User/*")
- r.Resource = cedar.NewEntityUID("HttpPath", "/.well-known/openid-configuration")
- r.Context = cedar.NewRecord(cedar.RecordMap{
- "host": cedar.String("idp.example.com"),
- })
- }),
- build(func(r *cedar.Request) {
- r.Principal = gid.NewEntityUID("gid://example/User/*")
- r.Resource = cedar.NewEntityUID("HttpPath", "/.well-known/oauth-authorization-server")
- r.Context = cedar.NewRecord(cedar.RecordMap{
- "host": cedar.String("idp.example.com"),
- })
- }),
- build(func(r *cedar.Request) {
- r.Principal = gid.NewEntityUID("gid://example/User/1")
- r.Action = cedar.NewEntityUID("HttpMethod", "POST")
- r.Resource = cedar.NewEntityUID("HttpPath", "/twirp/authz.rpc.Ability/Allowed")
- r.Context = cedar.NewRecord(cedar.RecordMap{
- "host": cedar.String("idp.example.com"),
- })
- }),
- build(func(r *cedar.Request) {
- r.Principal = gid.NewEntityUID("gid://example/User/1")
- r.Action = cedar.NewEntityUID("HttpMethod", "GET")
- r.Resource = cedar.NewEntityUID("HttpPath", "/index.html")
- r.Context = cedar.NewRecord(cedar.RecordMap{
- "host": cedar.String("ui.example.com"),
- })
- }),
- }
-
- for _, tt := range allowed {
- t.Run(fmt.Sprintf("allows: %v/%v %v %v%v", tt.Principal.Type, tt.Principal.ID, tt.Action.ID, tt.Context.Map()["host"], tt.Resource.ID), func(t *testing.T) {
- assert.True(t, Allowed(t.Context(), *tt))
- })
- }
-
- denied := []*cedar.Request{
- build(func(r *cedar.Request) {
- r.Principal = gid.ZeroEntityUID()
- r.Action = cedar.NewEntityUID("HttpMethod", cedar.String("POST"))
- }),
- build(func(r *cedar.Request) {
- r.Principal = gid.ZeroEntityUID()
- r.Action = cedar.NewEntityUID("HttpMethod", cedar.String("PUT"))
- }),
- build(func(r *cedar.Request) {
- r.Principal = gid.ZeroEntityUID()
- r.Action = cedar.NewEntityUID("HttpMethod", cedar.String("PATCH"))
- }),
- build(func(r *cedar.Request) {
- r.Principal = gid.ZeroEntityUID()
- r.Action = cedar.NewEntityUID("HttpMethod", cedar.String("DELETE"))
- }),
- build(func(r *cedar.Request) {
- r.Principal = gid.ZeroEntityUID()
- r.Action = cedar.NewEntityUID("HttpMethod", cedar.String("HEAD"))
- }),
- build(func(r *cedar.Request) {
- r.Principal = gid.ZeroEntityUID()
- r.Action = cedar.NewEntityUID("HttpMethod", cedar.String("TRACE"))
- }),
- }
-
- for _, tt := range denied {
- t.Run(fmt.Sprintf("denies: %v/%v %v %v%v", tt.Principal.Type, tt.Principal.ID, tt.Action.ID, tt.Context.Map()["host"], tt.Resource.ID), func(t *testing.T) {
- assert.False(t, Allowed(t.Context(), *tt))
- })
- }
-}
diff --git a/pkg/policies/entities.json b/pkg/policies/entities.json
deleted file mode 100644
index 8d50e674..00000000
--- a/pkg/policies/entities.json
+++ /dev/null
@@ -1,286 +0,0 @@
-[
- {
- "uid": {
- "type": "User",
- "id": "1"
- },
- "attrs": {},
- "parents": []
- },
- {
- "uid": {
- "type": "Organization",
- "id": "1"
- },
- "attrs": {
- "name": "default"
- },
- "parents": []
- },
- {
- "uid": {
- "type": "Organization",
- "id": "2"
- },
- "attrs": {
- "name": "gitlab"
- },
- "parents": []
- },
- {
- "uid": {
- "type": "Group",
- "id": "1"
- },
- "attrs": {
- "name": "A"
- },
- "parents": [
- {
- "type": "Organization",
- "id": "1"
- }
- ]
- },
- {
- "uid": {
- "type": "Group",
- "id": "2"
- },
- "attrs": {
- "name": "B"
- },
- "parents": [
- {
- "type": "Organization",
- "id": "1"
- }
- ]
- },
- {
- "uid": {
- "type": "Group",
- "id": "3"
- },
- "attrs": {
- "name": "gitlab-org"
- },
- "parents": [
- {
- "type": "Organization",
- "id": "2"
- }
- ]
- },
- {
- "uid": {
- "type": "Group",
- "id": "4"
- },
- "attrs": {
- "name": "gitlab-com"
- },
- "parents": [
- {
- "type": "Organization",
- "id": "2"
- }
- ]
- },
- {
- "uid": {
- "type": "Group",
- "id": "5"
- },
- "attrs": {
- "name": "gl-security"
- },
- "parents": [
- {
- "type": "Organization",
- "id": "2"
- },
- {
- "type": "Group",
- "id": "4"
- }
- ]
- },
- {
- "uid": {
- "type": "Group",
- "id": "6"
- },
- "attrs": {
- "name": "test-projects"
- },
- "parents": [
- {
- "type": "Organization",
- "id": "2"
- },
- {
- "type": "Group",
- "id": "5"
- }
- ]
- },
- {
- "uid": {
- "type": "Group",
- "id": "7"
- },
- "attrs": {
- "name": "support"
- },
- "parents": [
- {
- "type": "Organization",
- "id": "2"
- },
- {
- "type": "Group",
- "id": "4"
- }
- ]
- },
- {
- "uid": {
- "type": "Group",
- "id": "8"
- },
- "attrs": {
- "name": "toolbox"
- },
- "parents": [
- {
- "type": "Organization",
- "id": "2"
- },
- {
- "type": "Group",
- "id": "7"
- }
- ]
- },
- {
- "uid": {
- "type": "Project",
- "id": "1"
- },
- "attrs": {
- "name": "A1"
- },
- "parents": [
- {
- "type": "Group",
- "id": "1"
- }
- ]
- },
- {
- "uid": {
- "type": "Project",
- "id": "2"
- },
- "attrs": {
- "name": "B1"
- },
- "parents": [
- {
- "type": "Group",
- "id": "2"
- }
- ]
- },
- {
- "uid": {
- "type": "Project",
- "id": "3"
- },
- "attrs": {
- "name": "gitlab"
- },
- "parents": [
- {
- "type": "Group",
- "id": "3"
- }
- ]
- },
- {
- "uid": {
- "type": "Project",
- "id": "4"
- },
- "attrs": {
- "name": "eicar-test-project"
- },
- "parents": [
- {
- "type": "Group",
- "id": "6"
- }
- ]
- },
- {
- "uid": {
- "type": "Project",
- "id": "5"
- },
- "attrs": {
- "name": "disclosures"
- },
- "parents": [
- {
- "type": "Group",
- "id": "5"
- }
- ]
- },
- {
- "uid": {
- "type": "Project",
- "id": "6"
- },
- "attrs": {
- "name": "changelog-parser"
- },
- "parents": [
- {
- "type": "Group",
- "id": "8"
- }
- ]
- },
- {
- "uid": {
- "type": "Project",
- "id": "7"
- },
- "attrs": {
- "name": "handbook"
- },
- "parents": [
- {
- "type": "Group",
- "id": "4"
- }
- ]
- },
- {
- "uid": {
- "type": "Project",
- "id": "8"
- },
- "attrs": {
- "name": "www-gitlab-com"
- },
- "parents": [
- {
- "type": "Group",
- "id": "4"
- }
- ]
- }
-]
diff --git a/pkg/policies/gtwy.cedar b/pkg/policies/gtwy.cedar
deleted file mode 100644
index a236e08b..00000000
--- a/pkg/policies/gtwy.cedar
+++ /dev/null
@@ -1,12 +0,0 @@
-permit(
- principal is User,
- action in [
- HttpMethod::"DELETE",
- HttpMethod::"GET",
- HttpMethod::"HEAD",
- HttpMethod::"PATCH",
- HttpMethod::"POST",
- HttpMethod::"PUT"
- ],
- resource
-);
diff --git a/pkg/policies/init.go b/pkg/policies/init.go
deleted file mode 100644
index bc270763..00000000
--- a/pkg/policies/init.go
+++ /dev/null
@@ -1,60 +0,0 @@
-package policies
-
-import (
- "context"
- "embed"
- _ "embed"
- "io/fs"
- "strings"
-
- "github.com/cedar-policy/cedar-go"
- "github.com/xlgmokha/x/pkg/log"
-)
-
-//go:embed *.cedar *.json
-var files embed.FS
-
-var All *cedar.PolicySet = cedar.NewPolicySet()
-var Entities cedar.EntityMap = cedar.EntityMap{}
-
-func init() {
- err := fs.WalkDir(files, ".", func(path string, d fs.DirEntry, err error) error {
- if err != nil {
- return err
- }
-
- if d.IsDir() {
- return nil
- }
-
- if strings.HasSuffix(path, ".cedar") {
- content, err := fs.ReadFile(files, path)
- if err != nil {
- return err
- }
-
- policy := cedar.Policy{}
- if err := policy.UnmarshalCedar(content); err != nil {
- return err
- }
- policy.SetFilename(path)
-
- All.Add(cedar.PolicyID(path), &policy)
- } else if strings.HasSuffix(path, ".json") {
- content, err := fs.ReadFile(files, path)
- if err != nil {
- return err
- }
-
- if err := Entities.UnmarshalJSON(content); err != nil {
- return err
- }
- }
-
- return nil
- })
-
- if err != nil {
- log.WithFields(context.Background(), log.Fields{"error": err})
- }
-}
diff --git a/pkg/policies/organization.cedar b/pkg/policies/organization.cedar
deleted file mode 100644
index a853f4e4..00000000
--- a/pkg/policies/organization.cedar
+++ /dev/null
@@ -1,5 +0,0 @@
-permit (
- principal == User::"1",
- action == Permission::"read",
- resource == Organization::"2"
-);
diff --git a/pkg/rpc/ability.pb.go b/pkg/rpc/ability.pb.go
deleted file mode 100644
index 939719fc..00000000
--- a/pkg/rpc/ability.pb.go
+++ /dev/null
@@ -1,194 +0,0 @@
-// Code generated by protoc-gen-go. DO NOT EDIT.
-// versions:
-// protoc-gen-go v1.36.6
-// protoc v3.19.6
-// source: ability.proto
-
-package rpc
-
-import (
- protoreflect "google.golang.org/protobuf/reflect/protoreflect"
- protoimpl "google.golang.org/protobuf/runtime/protoimpl"
- reflect "reflect"
- sync "sync"
- unsafe "unsafe"
-)
-
-const (
- // Verify that this generated code is sufficiently up-to-date.
- _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
- // Verify that runtime/protoimpl is sufficiently up-to-date.
- _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
-)
-
-type AllowRequest struct {
- state protoimpl.MessageState `protogen:"open.v1"`
- Subject string `protobuf:"bytes,1,opt,name=subject,proto3" json:"subject,omitempty"`
- Permission string `protobuf:"bytes,2,opt,name=permission,proto3" json:"permission,omitempty"`
- Resource string `protobuf:"bytes,3,opt,name=resource,proto3" json:"resource,omitempty"`
- unknownFields protoimpl.UnknownFields
- sizeCache protoimpl.SizeCache
-}
-
-func (x *AllowRequest) Reset() {
- *x = AllowRequest{}
- mi := &file_ability_proto_msgTypes[0]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
-}
-
-func (x *AllowRequest) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*AllowRequest) ProtoMessage() {}
-
-func (x *AllowRequest) ProtoReflect() protoreflect.Message {
- mi := &file_ability_proto_msgTypes[0]
- if x != nil {
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- if ms.LoadMessageInfo() == nil {
- ms.StoreMessageInfo(mi)
- }
- return ms
- }
- return mi.MessageOf(x)
-}
-
-// Deprecated: Use AllowRequest.ProtoReflect.Descriptor instead.
-func (*AllowRequest) Descriptor() ([]byte, []int) {
- return file_ability_proto_rawDescGZIP(), []int{0}
-}
-
-func (x *AllowRequest) GetSubject() string {
- if x != nil {
- return x.Subject
- }
- return ""
-}
-
-func (x *AllowRequest) GetPermission() string {
- if x != nil {
- return x.Permission
- }
- return ""
-}
-
-func (x *AllowRequest) GetResource() string {
- if x != nil {
- return x.Resource
- }
- return ""
-}
-
-type AllowReply struct {
- state protoimpl.MessageState `protogen:"open.v1"`
- Result bool `protobuf:"varint,1,opt,name=result,proto3" json:"result,omitempty"`
- unknownFields protoimpl.UnknownFields
- sizeCache protoimpl.SizeCache
-}
-
-func (x *AllowReply) Reset() {
- *x = AllowReply{}
- mi := &file_ability_proto_msgTypes[1]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
-}
-
-func (x *AllowReply) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*AllowReply) ProtoMessage() {}
-
-func (x *AllowReply) ProtoReflect() protoreflect.Message {
- mi := &file_ability_proto_msgTypes[1]
- if x != nil {
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- if ms.LoadMessageInfo() == nil {
- ms.StoreMessageInfo(mi)
- }
- return ms
- }
- return mi.MessageOf(x)
-}
-
-// Deprecated: Use AllowReply.ProtoReflect.Descriptor instead.
-func (*AllowReply) Descriptor() ([]byte, []int) {
- return file_ability_proto_rawDescGZIP(), []int{1}
-}
-
-func (x *AllowReply) GetResult() bool {
- if x != nil {
- return x.Result
- }
- return false
-}
-
-var File_ability_proto protoreflect.FileDescriptor
-
-const file_ability_proto_rawDesc = "" +
- "\n" +
- "\rability.proto\x12\tauthz.rpc\"d\n" +
- "\fAllowRequest\x12\x18\n" +
- "\asubject\x18\x01 \x01(\tR\asubject\x12\x1e\n" +
- "\n" +
- "permission\x18\x02 \x01(\tR\n" +
- "permission\x12\x1a\n" +
- "\bresource\x18\x03 \x01(\tR\bresource\"$\n" +
- "\n" +
- "AllowReply\x12\x16\n" +
- "\x06result\x18\x01 \x01(\bR\x06result2F\n" +
- "\aAbility\x12;\n" +
- "\aAllowed\x12\x17.authz.rpc.AllowRequest\x1a\x15.authz.rpc.AllowReply\"\x00B\tZ\apkg/rpcb\x06proto3"
-
-var (
- file_ability_proto_rawDescOnce sync.Once
- file_ability_proto_rawDescData []byte
-)
-
-func file_ability_proto_rawDescGZIP() []byte {
- file_ability_proto_rawDescOnce.Do(func() {
- file_ability_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_ability_proto_rawDesc), len(file_ability_proto_rawDesc)))
- })
- return file_ability_proto_rawDescData
-}
-
-var file_ability_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
-var file_ability_proto_goTypes = []any{
- (*AllowRequest)(nil), // 0: authz.rpc.AllowRequest
- (*AllowReply)(nil), // 1: authz.rpc.AllowReply
-}
-var file_ability_proto_depIdxs = []int32{
- 0, // 0: authz.rpc.Ability.Allowed:input_type -> authz.rpc.AllowRequest
- 1, // 1: authz.rpc.Ability.Allowed:output_type -> authz.rpc.AllowReply
- 1, // [1:2] is the sub-list for method output_type
- 0, // [0:1] is the sub-list for method input_type
- 0, // [0:0] is the sub-list for extension type_name
- 0, // [0:0] is the sub-list for extension extendee
- 0, // [0:0] is the sub-list for field type_name
-}
-
-func init() { file_ability_proto_init() }
-func file_ability_proto_init() {
- if File_ability_proto != nil {
- return
- }
- type x struct{}
- out := protoimpl.TypeBuilder{
- File: protoimpl.DescBuilder{
- GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
- RawDescriptor: unsafe.Slice(unsafe.StringData(file_ability_proto_rawDesc), len(file_ability_proto_rawDesc)),
- NumEnums: 0,
- NumMessages: 2,
- NumExtensions: 0,
- NumServices: 1,
- },
- GoTypes: file_ability_proto_goTypes,
- DependencyIndexes: file_ability_proto_depIdxs,
- MessageInfos: file_ability_proto_msgTypes,
- }.Build()
- File_ability_proto = out.File
- file_ability_proto_goTypes = nil
- file_ability_proto_depIdxs = nil
-}
diff --git a/pkg/rpc/ability.twirp.go b/pkg/rpc/ability.twirp.go
deleted file mode 100644
index f5a33296..00000000
--- a/pkg/rpc/ability.twirp.go
+++ /dev/null
@@ -1,1104 +0,0 @@
-// Code generated by protoc-gen-twirp v8.1.3, DO NOT EDIT.
-// source: ability.proto
-
-package rpc
-
-import context "context"
-import fmt "fmt"
-import http "net/http"
-import io "io"
-import json "encoding/json"
-import strconv "strconv"
-import strings "strings"
-
-import protojson "google.golang.org/protobuf/encoding/protojson"
-import proto "google.golang.org/protobuf/proto"
-import twirp "github.com/twitchtv/twirp"
-import ctxsetters "github.com/twitchtv/twirp/ctxsetters"
-
-import bytes "bytes"
-import errors "errors"
-import path "path"
-import url "net/url"
-
-// Version compatibility assertion.
-// If the constant is not defined in the package, that likely means
-// the package needs to be updated to work with this generated code.
-// See https://twitchtv.github.io/twirp/docs/version_matrix.html
-const _ = twirp.TwirpPackageMinVersion_8_1_0
-
-// =================
-// Ability Interface
-// =================
-
-type Ability interface {
- Allowed(context.Context, *AllowRequest) (*AllowReply, error)
-}
-
-// =======================
-// Ability Protobuf Client
-// =======================
-
-type abilityProtobufClient struct {
- client HTTPClient
- urls [1]string
- interceptor twirp.Interceptor
- opts twirp.ClientOptions
-}
-
-// NewAbilityProtobufClient creates a Protobuf client that implements the Ability interface.
-// It communicates using Protobuf and can be configured with a custom HTTPClient.
-func NewAbilityProtobufClient(baseURL string, client HTTPClient, opts ...twirp.ClientOption) Ability {
- if c, ok := client.(*http.Client); ok {
- client = withoutRedirects(c)
- }
-
- clientOpts := twirp.ClientOptions{}
- for _, o := range opts {
- o(&clientOpts)
- }
-
- // Using ReadOpt allows backwards and forwards compatibility with new options in the future
- literalURLs := false
- _ = clientOpts.ReadOpt("literalURLs", &literalURLs)
- var pathPrefix string
- if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok {
- pathPrefix = "/twirp" // default prefix
- }
-
- // Build method URLs: <baseURL>[<prefix>]/<package>.<Service>/<Method>
- serviceURL := sanitizeBaseURL(baseURL)
- serviceURL += baseServicePath(pathPrefix, "authz.rpc", "Ability")
- urls := [1]string{
- serviceURL + "Allowed",
- }
-
- return &abilityProtobufClient{
- client: client,
- urls: urls,
- interceptor: twirp.ChainInterceptors(clientOpts.Interceptors...),
- opts: clientOpts,
- }
-}
-
-func (c *abilityProtobufClient) Allowed(ctx context.Context, in *AllowRequest) (*AllowReply, error) {
- ctx = ctxsetters.WithPackageName(ctx, "authz.rpc")
- ctx = ctxsetters.WithServiceName(ctx, "Ability")
- ctx = ctxsetters.WithMethodName(ctx, "Allowed")
- caller := c.callAllowed
- if c.interceptor != nil {
- caller = func(ctx context.Context, req *AllowRequest) (*AllowReply, error) {
- resp, err := c.interceptor(
- func(ctx context.Context, req interface{}) (interface{}, error) {
- typedReq, ok := req.(*AllowRequest)
- if !ok {
- return nil, twirp.InternalError("failed type assertion req.(*AllowRequest) when calling interceptor")
- }
- return c.callAllowed(ctx, typedReq)
- },
- )(ctx, req)
- if resp != nil {
- typedResp, ok := resp.(*AllowReply)
- if !ok {
- return nil, twirp.InternalError("failed type assertion resp.(*AllowReply) when calling interceptor")
- }
- return typedResp, err
- }
- return nil, err
- }
- }
- return caller(ctx, in)
-}
-
-func (c *abilityProtobufClient) callAllowed(ctx context.Context, in *AllowRequest) (*AllowReply, error) {
- out := new(AllowReply)
- ctx, err := doProtobufRequest(ctx, c.client, c.opts.Hooks, c.urls[0], in, out)
- if err != nil {
- twerr, ok := err.(twirp.Error)
- if !ok {
- twerr = twirp.InternalErrorWith(err)
- }
- callClientError(ctx, c.opts.Hooks, twerr)
- return nil, err
- }
-
- callClientResponseReceived(ctx, c.opts.Hooks)
-
- return out, nil
-}
-
-// ===================
-// Ability JSON Client
-// ===================
-
-type abilityJSONClient struct {
- client HTTPClient
- urls [1]string
- interceptor twirp.Interceptor
- opts twirp.ClientOptions
-}
-
-// NewAbilityJSONClient creates a JSON client that implements the Ability interface.
-// It communicates using JSON and can be configured with a custom HTTPClient.
-func NewAbilityJSONClient(baseURL string, client HTTPClient, opts ...twirp.ClientOption) Ability {
- if c, ok := client.(*http.Client); ok {
- client = withoutRedirects(c)
- }
-
- clientOpts := twirp.ClientOptions{}
- for _, o := range opts {
- o(&clientOpts)
- }
-
- // Using ReadOpt allows backwards and forwards compatibility with new options in the future
- literalURLs := false
- _ = clientOpts.ReadOpt("literalURLs", &literalURLs)
- var pathPrefix string
- if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok {
- pathPrefix = "/twirp" // default prefix
- }
-
- // Build method URLs: <baseURL>[<prefix>]/<package>.<Service>/<Method>
- serviceURL := sanitizeBaseURL(baseURL)
- serviceURL += baseServicePath(pathPrefix, "authz.rpc", "Ability")
- urls := [1]string{
- serviceURL + "Allowed",
- }
-
- return &abilityJSONClient{
- client: client,
- urls: urls,
- interceptor: twirp.ChainInterceptors(clientOpts.Interceptors...),
- opts: clientOpts,
- }
-}
-
-func (c *abilityJSONClient) Allowed(ctx context.Context, in *AllowRequest) (*AllowReply, error) {
- ctx = ctxsetters.WithPackageName(ctx, "authz.rpc")
- ctx = ctxsetters.WithServiceName(ctx, "Ability")
- ctx = ctxsetters.WithMethodName(ctx, "Allowed")
- caller := c.callAllowed
- if c.interceptor != nil {
- caller = func(ctx context.Context, req *AllowRequest) (*AllowReply, error) {
- resp, err := c.interceptor(
- func(ctx context.Context, req interface{}) (interface{}, error) {
- typedReq, ok := req.(*AllowRequest)
- if !ok {
- return nil, twirp.InternalError("failed type assertion req.(*AllowRequest) when calling interceptor")
- }
- return c.callAllowed(ctx, typedReq)
- },
- )(ctx, req)
- if resp != nil {
- typedResp, ok := resp.(*AllowReply)
- if !ok {
- return nil, twirp.InternalError("failed type assertion resp.(*AllowReply) when calling interceptor")
- }
- return typedResp, err
- }
- return nil, err
- }
- }
- return caller(ctx, in)
-}
-
-func (c *abilityJSONClient) callAllowed(ctx context.Context, in *AllowRequest) (*AllowReply, error) {
- out := new(AllowReply)
- ctx, err := doJSONRequest(ctx, c.client, c.opts.Hooks, c.urls[0], in, out)
- if err != nil {
- twerr, ok := err.(twirp.Error)
- if !ok {
- twerr = twirp.InternalErrorWith(err)
- }
- callClientError(ctx, c.opts.Hooks, twerr)
- return nil, err
- }
-
- callClientResponseReceived(ctx, c.opts.Hooks)
-
- return out, nil
-}
-
-// ======================
-// Ability Server Handler
-// ======================
-
-type abilityServer struct {
- Ability
- interceptor twirp.Interceptor
- hooks *twirp.ServerHooks
- pathPrefix string // prefix for routing
- jsonSkipDefaults bool // do not include unpopulated fields (default values) in the response
- jsonCamelCase bool // JSON fields are serialized as lowerCamelCase rather than keeping the original proto names
-}
-
-// NewAbilityServer builds a TwirpServer that can be used as an http.Handler to handle
-// HTTP requests that are routed to the right method in the provided svc implementation.
-// The opts are twirp.ServerOption modifiers, for example twirp.WithServerHooks(hooks).
-func NewAbilityServer(svc Ability, opts ...interface{}) TwirpServer {
- serverOpts := newServerOpts(opts)
-
- // Using ReadOpt allows backwards and forwards compatibility with new options in the future
- jsonSkipDefaults := false
- _ = serverOpts.ReadOpt("jsonSkipDefaults", &jsonSkipDefaults)
- jsonCamelCase := false
- _ = serverOpts.ReadOpt("jsonCamelCase", &jsonCamelCase)
- var pathPrefix string
- if ok := serverOpts.ReadOpt("pathPrefix", &pathPrefix); !ok {
- pathPrefix = "/twirp" // default prefix
- }
-
- return &abilityServer{
- Ability: svc,
- hooks: serverOpts.Hooks,
- interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...),
- pathPrefix: pathPrefix,
- jsonSkipDefaults: jsonSkipDefaults,
- jsonCamelCase: jsonCamelCase,
- }
-}
-
-// writeError writes an HTTP response with a valid Twirp error format, and triggers hooks.
-// If err is not a twirp.Error, it will get wrapped with twirp.InternalErrorWith(err)
-func (s *abilityServer) writeError(ctx context.Context, resp http.ResponseWriter, err error) {
- writeError(ctx, resp, err, s.hooks)
-}
-
-// handleRequestBodyError is used to handle error when the twirp server cannot read request
-func (s *abilityServer) handleRequestBodyError(ctx context.Context, resp http.ResponseWriter, msg string, err error) {
- if context.Canceled == ctx.Err() {
- s.writeError(ctx, resp, twirp.NewError(twirp.Canceled, "failed to read request: context canceled"))
- return
- }
- if context.DeadlineExceeded == ctx.Err() {
- s.writeError(ctx, resp, twirp.NewError(twirp.DeadlineExceeded, "failed to read request: deadline exceeded"))
- return
- }
- s.writeError(ctx, resp, twirp.WrapError(malformedRequestError(msg), err))
-}
-
-// AbilityPathPrefix is a convenience constant that may identify URL paths.
-// Should be used with caution, it only matches routes generated by Twirp Go clients,
-// with the default "/twirp" prefix and default CamelCase service and method names.
-// More info: https://twitchtv.github.io/twirp/docs/routing.html
-const AbilityPathPrefix = "/twirp/authz.rpc.Ability/"
-
-func (s *abilityServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
- ctx := req.Context()
- ctx = ctxsetters.WithPackageName(ctx, "authz.rpc")
- ctx = ctxsetters.WithServiceName(ctx, "Ability")
- ctx = ctxsetters.WithResponseWriter(ctx, resp)
-
- var err error
- ctx, err = callRequestReceived(ctx, s.hooks)
- if err != nil {
- s.writeError(ctx, resp, err)
- return
- }
-
- if req.Method != "POST" {
- msg := fmt.Sprintf("unsupported method %q (only POST is allowed)", req.Method)
- s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path))
- return
- }
-
- // Verify path format: [<prefix>]/<package>.<Service>/<Method>
- prefix, pkgService, method := parseTwirpPath(req.URL.Path)
- if pkgService != "authz.rpc.Ability" {
- msg := fmt.Sprintf("no handler for path %q", req.URL.Path)
- s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path))
- return
- }
- if prefix != s.pathPrefix {
- msg := fmt.Sprintf("invalid path prefix %q, expected %q, on path %q", prefix, s.pathPrefix, req.URL.Path)
- s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path))
- return
- }
-
- switch method {
- case "Allowed":
- s.serveAllowed(ctx, resp, req)
- return
- default:
- msg := fmt.Sprintf("no handler for path %q", req.URL.Path)
- s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path))
- return
- }
-}
-
-func (s *abilityServer) serveAllowed(ctx context.Context, resp http.ResponseWriter, req *http.Request) {
- header := req.Header.Get("Content-Type")
- i := strings.Index(header, ";")
- if i == -1 {
- i = len(header)
- }
- switch strings.TrimSpace(strings.ToLower(header[:i])) {
- case "application/json":
- s.serveAllowedJSON(ctx, resp, req)
- case "application/protobuf":
- s.serveAllowedProtobuf(ctx, resp, req)
- default:
- msg := fmt.Sprintf("unexpected Content-Type: %q", req.Header.Get("Content-Type"))
- twerr := badRouteError(msg, req.Method, req.URL.Path)
- s.writeError(ctx, resp, twerr)
- }
-}
-
-func (s *abilityServer) serveAllowedJSON(ctx context.Context, resp http.ResponseWriter, req *http.Request) {
- var err error
- ctx = ctxsetters.WithMethodName(ctx, "Allowed")
- ctx, err = callRequestRouted(ctx, s.hooks)
- if err != nil {
- s.writeError(ctx, resp, err)
- return
- }
-
- d := json.NewDecoder(req.Body)
- rawReqBody := json.RawMessage{}
- if err := d.Decode(&rawReqBody); err != nil {
- s.handleRequestBodyError(ctx, resp, "the json request could not be decoded", err)
- return
- }
- reqContent := new(AllowRequest)
- unmarshaler := protojson.UnmarshalOptions{DiscardUnknown: true}
- if err = unmarshaler.Unmarshal(rawReqBody, reqContent); err != nil {
- s.handleRequestBodyError(ctx, resp, "the json request could not be decoded", err)
- return
- }
-
- handler := s.Ability.Allowed
- if s.interceptor != nil {
- handler = func(ctx context.Context, req *AllowRequest) (*AllowReply, error) {
- resp, err := s.interceptor(
- func(ctx context.Context, req interface{}) (interface{}, error) {
- typedReq, ok := req.(*AllowRequest)
- if !ok {
- return nil, twirp.InternalError("failed type assertion req.(*AllowRequest) when calling interceptor")
- }
- return s.Ability.Allowed(ctx, typedReq)
- },
- )(ctx, req)
- if resp != nil {
- typedResp, ok := resp.(*AllowReply)
- if !ok {
- return nil, twirp.InternalError("failed type assertion resp.(*AllowReply) when calling interceptor")
- }
- return typedResp, err
- }
- return nil, err
- }
- }
-
- // Call service method
- var respContent *AllowReply
- func() {
- defer ensurePanicResponses(ctx, resp, s.hooks)
- respContent, err = handler(ctx, reqContent)
- }()
-
- if err != nil {
- s.writeError(ctx, resp, err)
- return
- }
- if respContent == nil {
- s.writeError(ctx, resp, twirp.InternalError("received a nil *AllowReply and nil error while calling Allowed. nil responses are not supported"))
- return
- }
-
- ctx = callResponsePrepared(ctx, s.hooks)
-
- marshaler := &protojson.MarshalOptions{UseProtoNames: !s.jsonCamelCase, EmitUnpopulated: !s.jsonSkipDefaults}
- respBytes, err := marshaler.Marshal(respContent)
- if err != nil {
- s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response"))
- return
- }
-
- ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK)
- resp.Header().Set("Content-Type", "application/json")
- resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes)))
- resp.WriteHeader(http.StatusOK)
-
- if n, err := resp.Write(respBytes); err != nil {
- msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error())
- twerr := twirp.NewError(twirp.Unknown, msg)
- ctx = callError(ctx, s.hooks, twerr)
- }
- callResponseSent(ctx, s.hooks)
-}
-
-func (s *abilityServer) serveAllowedProtobuf(ctx context.Context, resp http.ResponseWriter, req *http.Request) {
- var err error
- ctx = ctxsetters.WithMethodName(ctx, "Allowed")
- ctx, err = callRequestRouted(ctx, s.hooks)
- if err != nil {
- s.writeError(ctx, resp, err)
- return
- }
-
- buf, err := io.ReadAll(req.Body)
- if err != nil {
- s.handleRequestBodyError(ctx, resp, "failed to read request body", err)
- return
- }
- reqContent := new(AllowRequest)
- if err = proto.Unmarshal(buf, reqContent); err != nil {
- s.writeError(ctx, resp, malformedRequestError("the protobuf request could not be decoded"))
- return
- }
-
- handler := s.Ability.Allowed
- if s.interceptor != nil {
- handler = func(ctx context.Context, req *AllowRequest) (*AllowReply, error) {
- resp, err := s.interceptor(
- func(ctx context.Context, req interface{}) (interface{}, error) {
- typedReq, ok := req.(*AllowRequest)
- if !ok {
- return nil, twirp.InternalError("failed type assertion req.(*AllowRequest) when calling interceptor")
- }
- return s.Ability.Allowed(ctx, typedReq)
- },
- )(ctx, req)
- if resp != nil {
- typedResp, ok := resp.(*AllowReply)
- if !ok {
- return nil, twirp.InternalError("failed type assertion resp.(*AllowReply) when calling interceptor")
- }
- return typedResp, err
- }
- return nil, err
- }
- }
-
- // Call service method
- var respContent *AllowReply
- func() {
- defer ensurePanicResponses(ctx, resp, s.hooks)
- respContent, err = handler(ctx, reqContent)
- }()
-
- if err != nil {
- s.writeError(ctx, resp, err)
- return
- }
- if respContent == nil {
- s.writeError(ctx, resp, twirp.InternalError("received a nil *AllowReply and nil error while calling Allowed. nil responses are not supported"))
- return
- }
-
- ctx = callResponsePrepared(ctx, s.hooks)
-
- respBytes, err := proto.Marshal(respContent)
- if err != nil {
- s.writeError(ctx, resp, wrapInternal(err, "failed to marshal proto response"))
- return
- }
-
- ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK)
- resp.Header().Set("Content-Type", "application/protobuf")
- resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes)))
- resp.WriteHeader(http.StatusOK)
- if n, err := resp.Write(respBytes); err != nil {
- msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error())
- twerr := twirp.NewError(twirp.Unknown, msg)
- ctx = callError(ctx, s.hooks, twerr)
- }
- callResponseSent(ctx, s.hooks)
-}
-
-func (s *abilityServer) ServiceDescriptor() ([]byte, int) {
- return twirpFileDescriptor0, 0
-}
-
-func (s *abilityServer) ProtocGenTwirpVersion() string {
- return "v8.1.3"
-}
-
-// PathPrefix returns the base service path, in the form: "/<prefix>/<package>.<Service>/"
-// that is everything in a Twirp route except for the <Method>. This can be used for routing,
-// for example to identify the requests that are targeted to this service in a mux.
-func (s *abilityServer) PathPrefix() string {
- return baseServicePath(s.pathPrefix, "authz.rpc", "Ability")
-}
-
-// =====
-// Utils
-// =====
-
-// HTTPClient is the interface used by generated clients to send HTTP requests.
-// It is fulfilled by *(net/http).Client, which is sufficient for most users.
-// Users can provide their own implementation for special retry policies.
-//
-// HTTPClient implementations should not follow redirects. Redirects are
-// automatically disabled if *(net/http).Client is passed to client
-// constructors. See the withoutRedirects function in this file for more
-// details.
-type HTTPClient interface {
- Do(req *http.Request) (*http.Response, error)
-}
-
-// TwirpServer is the interface generated server structs will support: they're
-// HTTP handlers with additional methods for accessing metadata about the
-// service. Those accessors are a low-level API for building reflection tools.
-// Most people can think of TwirpServers as just http.Handlers.
-type TwirpServer interface {
- http.Handler
-
- // ServiceDescriptor returns gzipped bytes describing the .proto file that
- // this service was generated from. Once unzipped, the bytes can be
- // unmarshalled as a
- // google.golang.org/protobuf/types/descriptorpb.FileDescriptorProto.
- //
- // The returned integer is the index of this particular service within that
- // FileDescriptorProto's 'Service' slice of ServiceDescriptorProtos. This is a
- // low-level field, expected to be used for reflection.
- ServiceDescriptor() ([]byte, int)
-
- // ProtocGenTwirpVersion is the semantic version string of the version of
- // twirp used to generate this file.
- ProtocGenTwirpVersion() string
-
- // PathPrefix returns the HTTP URL path prefix for all methods handled by this
- // service. This can be used with an HTTP mux to route Twirp requests.
- // The path prefix is in the form: "/<prefix>/<package>.<Service>/"
- // that is, everything in a Twirp route except for the <Method> at the end.
- PathPrefix() string
-}
-
-func newServerOpts(opts []interface{}) *twirp.ServerOptions {
- serverOpts := &twirp.ServerOptions{}
- for _, opt := range opts {
- switch o := opt.(type) {
- case twirp.ServerOption:
- o(serverOpts)
- case *twirp.ServerHooks: // backwards compatibility, allow to specify hooks as an argument
- twirp.WithServerHooks(o)(serverOpts)
- case nil: // backwards compatibility, allow nil value for the argument
- continue
- default:
- panic(fmt.Sprintf("Invalid option type %T, please use a twirp.ServerOption", o))
- }
- }
- return serverOpts
-}
-
-// WriteError writes an HTTP response with a valid Twirp error format (code, msg, meta).
-// Useful outside of the Twirp server (e.g. http middleware), but does not trigger hooks.
-// If err is not a twirp.Error, it will get wrapped with twirp.InternalErrorWith(err)
-func WriteError(resp http.ResponseWriter, err error) {
- writeError(context.Background(), resp, err, nil)
-}
-
-// writeError writes Twirp errors in the response and triggers hooks.
-func writeError(ctx context.Context, resp http.ResponseWriter, err error, hooks *twirp.ServerHooks) {
- // Convert to a twirp.Error. Non-twirp errors are converted to internal errors.
- var twerr twirp.Error
- if !errors.As(err, &twerr) {
- twerr = twirp.InternalErrorWith(err)
- }
-
- statusCode := twirp.ServerHTTPStatusFromErrorCode(twerr.Code())
- ctx = ctxsetters.WithStatusCode(ctx, statusCode)
- ctx = callError(ctx, hooks, twerr)
-
- respBody := marshalErrorToJSON(twerr)
-
- resp.Header().Set("Content-Type", "application/json") // Error responses are always JSON
- resp.Header().Set("Content-Length", strconv.Itoa(len(respBody)))
- resp.WriteHeader(statusCode) // set HTTP status code and send response
-
- _, writeErr := resp.Write(respBody)
- if writeErr != nil {
- // We have three options here. We could log the error, call the Error
- // hook, or just silently ignore the error.
- //
- // Logging is unacceptable because we don't have a user-controlled
- // logger; writing out to stderr without permission is too rude.
- //
- // Calling the Error hook would confuse users: it would mean the Error
- // hook got called twice for one request, which is likely to lead to
- // duplicated log messages and metrics, no matter how well we document
- // the behavior.
- //
- // Silently ignoring the error is our least-bad option. It's highly
- // likely that the connection is broken and the original 'err' says
- // so anyway.
- _ = writeErr
- }
-
- callResponseSent(ctx, hooks)
-}
-
-// sanitizeBaseURL parses the the baseURL, and adds the "http" scheme if needed.
-// If the URL is unparsable, the baseURL is returned unchanged.
-func sanitizeBaseURL(baseURL string) string {
- u, err := url.Parse(baseURL)
- if err != nil {
- return baseURL // invalid URL will fail later when making requests
- }
- if u.Scheme == "" {
- u.Scheme = "http"
- }
- return u.String()
-}
-
-// baseServicePath composes the path prefix for the service (without <Method>).
-// e.g.: baseServicePath("/twirp", "my.pkg", "MyService")
-//
-// returns => "/twirp/my.pkg.MyService/"
-//
-// e.g.: baseServicePath("", "", "MyService")
-//
-// returns => "/MyService/"
-func baseServicePath(prefix, pkg, service string) string {
- fullServiceName := service
- if pkg != "" {
- fullServiceName = pkg + "." + service
- }
- return path.Join("/", prefix, fullServiceName) + "/"
-}
-
-// parseTwirpPath extracts path components form a valid Twirp route.
-// Expected format: "[<prefix>]/<package>.<Service>/<Method>"
-// e.g.: prefix, pkgService, method := parseTwirpPath("/twirp/pkg.Svc/MakeHat")
-func parseTwirpPath(path string) (string, string, string) {
- parts := strings.Split(path, "/")
- if len(parts) < 2 {
- return "", "", ""
- }
- method := parts[len(parts)-1]
- pkgService := parts[len(parts)-2]
- prefix := strings.Join(parts[0:len(parts)-2], "/")
- return prefix, pkgService, method
-}
-
-// getCustomHTTPReqHeaders retrieves a copy of any headers that are set in
-// a context through the twirp.WithHTTPRequestHeaders function.
-// If there are no headers set, or if they have the wrong type, nil is returned.
-func getCustomHTTPReqHeaders(ctx context.Context) http.Header {
- header, ok := twirp.HTTPRequestHeaders(ctx)
- if !ok || header == nil {
- return nil
- }
- copied := make(http.Header)
- for k, vv := range header {
- if vv == nil {
- copied[k] = nil
- continue
- }
- copied[k] = make([]string, len(vv))
- copy(copied[k], vv)
- }
- return copied
-}
-
-// newRequest makes an http.Request from a client, adding common headers.
-func newRequest(ctx context.Context, url string, reqBody io.Reader, contentType string) (*http.Request, error) {
- req, err := http.NewRequest("POST", url, reqBody)
- if err != nil {
- return nil, err
- }
- req = req.WithContext(ctx)
- if customHeader := getCustomHTTPReqHeaders(ctx); customHeader != nil {
- req.Header = customHeader
- }
- req.Header.Set("Accept", contentType)
- req.Header.Set("Content-Type", contentType)
- req.Header.Set("Twirp-Version", "v8.1.3")
- return req, nil
-}
-
-// JSON serialization for errors
-type twerrJSON struct {
- Code string `json:"code"`
- Msg string `json:"msg"`
- Meta map[string]string `json:"meta,omitempty"`
-}
-
-// marshalErrorToJSON returns JSON from a twirp.Error, that can be used as HTTP error response body.
-// If serialization fails, it will use a descriptive Internal error instead.
-func marshalErrorToJSON(twerr twirp.Error) []byte {
- // make sure that msg is not too large
- msg := twerr.Msg()
- if len(msg) > 1e6 {
- msg = msg[:1e6]
- }
-
- tj := twerrJSON{
- Code: string(twerr.Code()),
- Msg: msg,
- Meta: twerr.MetaMap(),
- }
-
- buf, err := json.Marshal(&tj)
- if err != nil {
- buf = []byte("{\"type\": \"" + twirp.Internal + "\", \"msg\": \"There was an error but it could not be serialized into JSON\"}") // fallback
- }
-
- return buf
-}
-
-// errorFromResponse builds a twirp.Error from a non-200 HTTP response.
-// If the response has a valid serialized Twirp error, then it's returned.
-// If not, the response status code is used to generate a similar twirp
-// error. See twirpErrorFromIntermediary for more info on intermediary errors.
-func errorFromResponse(resp *http.Response) twirp.Error {
- statusCode := resp.StatusCode
- statusText := http.StatusText(statusCode)
-
- if isHTTPRedirect(statusCode) {
- // Unexpected redirect: it must be an error from an intermediary.
- // Twirp clients don't follow redirects automatically, Twirp only handles
- // POST requests, redirects should only happen on GET and HEAD requests.
- location := resp.Header.Get("Location")
- msg := fmt.Sprintf("unexpected HTTP status code %d %q received, Location=%q", statusCode, statusText, location)
- return twirpErrorFromIntermediary(statusCode, msg, location)
- }
-
- respBodyBytes, err := io.ReadAll(resp.Body)
- if err != nil {
- return wrapInternal(err, "failed to read server error response body")
- }
-
- var tj twerrJSON
- dec := json.NewDecoder(bytes.NewReader(respBodyBytes))
- dec.DisallowUnknownFields()
- if err := dec.Decode(&tj); err != nil || tj.Code == "" {
- // Invalid JSON response; it must be an error from an intermediary.
- msg := fmt.Sprintf("Error from intermediary with HTTP status code %d %q", statusCode, statusText)
- return twirpErrorFromIntermediary(statusCode, msg, string(respBodyBytes))
- }
-
- errorCode := twirp.ErrorCode(tj.Code)
- if !twirp.IsValidErrorCode(errorCode) {
- msg := "invalid type returned from server error response: " + tj.Code
- return twirp.InternalError(msg).WithMeta("body", string(respBodyBytes))
- }
-
- twerr := twirp.NewError(errorCode, tj.Msg)
- for k, v := range tj.Meta {
- twerr = twerr.WithMeta(k, v)
- }
- return twerr
-}
-
-// twirpErrorFromIntermediary maps HTTP errors from non-twirp sources to twirp errors.
-// The mapping is similar to gRPC: https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md.
-// Returned twirp Errors have some additional metadata for inspection.
-func twirpErrorFromIntermediary(status int, msg string, bodyOrLocation string) twirp.Error {
- var code twirp.ErrorCode
- if isHTTPRedirect(status) { // 3xx
- code = twirp.Internal
- } else {
- switch status {
- case 400: // Bad Request
- code = twirp.Internal
- case 401: // Unauthorized
- code = twirp.Unauthenticated
- case 403: // Forbidden
- code = twirp.PermissionDenied
- case 404: // Not Found
- code = twirp.BadRoute
- case 429: // Too Many Requests
- code = twirp.ResourceExhausted
- case 502, 503, 504: // Bad Gateway, Service Unavailable, Gateway Timeout
- code = twirp.Unavailable
- default: // All other codes
- code = twirp.Unknown
- }
- }
-
- twerr := twirp.NewError(code, msg)
- twerr = twerr.WithMeta("http_error_from_intermediary", "true") // to easily know if this error was from intermediary
- twerr = twerr.WithMeta("status_code", strconv.Itoa(status))
- if isHTTPRedirect(status) {
- twerr = twerr.WithMeta("location", bodyOrLocation)
- } else {
- twerr = twerr.WithMeta("body", bodyOrLocation)
- }
- return twerr
-}
-
-func isHTTPRedirect(status int) bool {
- return status >= 300 && status <= 399
-}
-
-// wrapInternal wraps an error with a prefix as an Internal error.
-// The original error cause is accessible by github.com/pkg/errors.Cause.
-func wrapInternal(err error, prefix string) twirp.Error {
- return twirp.InternalErrorWith(&wrappedError{prefix: prefix, cause: err})
-}
-
-type wrappedError struct {
- prefix string
- cause error
-}
-
-func (e *wrappedError) Error() string { return e.prefix + ": " + e.cause.Error() }
-func (e *wrappedError) Unwrap() error { return e.cause } // for go1.13 + errors.Is/As
-func (e *wrappedError) Cause() error { return e.cause } // for github.com/pkg/errors
-
-// ensurePanicResponses makes sure that rpc methods causing a panic still result in a Twirp Internal
-// error response (status 500), and error hooks are properly called with the panic wrapped as an error.
-// The panic is re-raised so it can be handled normally with middleware.
-func ensurePanicResponses(ctx context.Context, resp http.ResponseWriter, hooks *twirp.ServerHooks) {
- if r := recover(); r != nil {
- // Wrap the panic as an error so it can be passed to error hooks.
- // The original error is accessible from error hooks, but not visible in the response.
- err := errFromPanic(r)
- twerr := &internalWithCause{msg: "Internal service panic", cause: err}
- // Actually write the error
- writeError(ctx, resp, twerr, hooks)
- // If possible, flush the error to the wire.
- f, ok := resp.(http.Flusher)
- if ok {
- f.Flush()
- }
-
- panic(r)
- }
-}
-
-// errFromPanic returns the typed error if the recovered panic is an error, otherwise formats as error.
-func errFromPanic(p interface{}) error {
- if err, ok := p.(error); ok {
- return err
- }
- return fmt.Errorf("panic: %v", p)
-}
-
-// internalWithCause is a Twirp Internal error wrapping an original error cause,
-// but the original error message is not exposed on Msg(). The original error
-// can be checked with go1.13+ errors.Is/As, and also by (github.com/pkg/errors).Unwrap
-type internalWithCause struct {
- msg string
- cause error
-}
-
-func (e *internalWithCause) Unwrap() error { return e.cause } // for go1.13 + errors.Is/As
-func (e *internalWithCause) Cause() error { return e.cause } // for github.com/pkg/errors
-func (e *internalWithCause) Error() string { return e.msg + ": " + e.cause.Error() }
-func (e *internalWithCause) Code() twirp.ErrorCode { return twirp.Internal }
-func (e *internalWithCause) Msg() string { return e.msg }
-func (e *internalWithCause) Meta(key string) string { return "" }
-func (e *internalWithCause) MetaMap() map[string]string { return nil }
-func (e *internalWithCause) WithMeta(key string, val string) twirp.Error { return e }
-
-// malformedRequestError is used when the twirp server cannot unmarshal a request
-func malformedRequestError(msg string) twirp.Error {
- return twirp.NewError(twirp.Malformed, msg)
-}
-
-// badRouteError is used when the twirp server cannot route a request
-func badRouteError(msg string, method, url string) twirp.Error {
- err := twirp.NewError(twirp.BadRoute, msg)
- err = err.WithMeta("twirp_invalid_route", method+" "+url)
- return err
-}
-
-// withoutRedirects makes sure that the POST request can not be redirected.
-// The standard library will, by default, redirect requests (including POSTs) if it gets a 302 or
-// 303 response, and also 301s in go1.8. It redirects by making a second request, changing the
-// method to GET and removing the body. This produces very confusing error messages, so instead we
-// set a redirect policy that always errors. This stops Go from executing the redirect.
-//
-// We have to be a little careful in case the user-provided http.Client has its own CheckRedirect
-// policy - if so, we'll run through that policy first.
-//
-// Because this requires modifying the http.Client, we make a new copy of the client and return it.
-func withoutRedirects(in *http.Client) *http.Client {
- copy := *in
- copy.CheckRedirect = func(req *http.Request, via []*http.Request) error {
- if in.CheckRedirect != nil {
- // Run the input's redirect if it exists, in case it has side effects, but ignore any error it
- // returns, since we want to use ErrUseLastResponse.
- err := in.CheckRedirect(req, via)
- _ = err // Silly, but this makes sure generated code passes errcheck -blank, which some people use.
- }
- return http.ErrUseLastResponse
- }
- return &copy
-}
-
-// doProtobufRequest makes a Protobuf request to the remote Twirp service.
-func doProtobufRequest(ctx context.Context, client HTTPClient, hooks *twirp.ClientHooks, url string, in, out proto.Message) (_ context.Context, err error) {
- reqBodyBytes, err := proto.Marshal(in)
- if err != nil {
- return ctx, wrapInternal(err, "failed to marshal proto request")
- }
- reqBody := bytes.NewBuffer(reqBodyBytes)
- if err = ctx.Err(); err != nil {
- return ctx, wrapInternal(err, "aborted because context was done")
- }
-
- req, err := newRequest(ctx, url, reqBody, "application/protobuf")
- if err != nil {
- return ctx, wrapInternal(err, "could not build request")
- }
- ctx, err = callClientRequestPrepared(ctx, hooks, req)
- if err != nil {
- return ctx, err
- }
-
- req = req.WithContext(ctx)
- resp, err := client.Do(req)
- if err != nil {
- return ctx, wrapInternal(err, "failed to do request")
- }
- defer func() { _ = resp.Body.Close() }()
-
- if err = ctx.Err(); err != nil {
- return ctx, wrapInternal(err, "aborted because context was done")
- }
-
- if resp.StatusCode != 200 {
- return ctx, errorFromResponse(resp)
- }
-
- respBodyBytes, err := io.ReadAll(resp.Body)
- if err != nil {
- return ctx, wrapInternal(err, "failed to read response body")
- }
- if err = ctx.Err(); err != nil {
- return ctx, wrapInternal(err, "aborted because context was done")
- }
-
- if err = proto.Unmarshal(respBodyBytes, out); err != nil {
- return ctx, wrapInternal(err, "failed to unmarshal proto response")
- }
- return ctx, nil
-}
-
-// doJSONRequest makes a JSON request to the remote Twirp service.
-func doJSONRequest(ctx context.Context, client HTTPClient, hooks *twirp.ClientHooks, url string, in, out proto.Message) (_ context.Context, err error) {
- marshaler := &protojson.MarshalOptions{UseProtoNames: true}
- reqBytes, err := marshaler.Marshal(in)
- if err != nil {
- return ctx, wrapInternal(err, "failed to marshal json request")
- }
- if err = ctx.Err(); err != nil {
- return ctx, wrapInternal(err, "aborted because context was done")
- }
-
- req, err := newRequest(ctx, url, bytes.NewReader(reqBytes), "application/json")
- if err != nil {
- return ctx, wrapInternal(err, "could not build request")
- }
- ctx, err = callClientRequestPrepared(ctx, hooks, req)
- if err != nil {
- return ctx, err
- }
-
- req = req.WithContext(ctx)
- resp, err := client.Do(req)
- if err != nil {
- return ctx, wrapInternal(err, "failed to do request")
- }
-
- defer func() {
- cerr := resp.Body.Close()
- if err == nil && cerr != nil {
- err = wrapInternal(cerr, "failed to close response body")
- }
- }()
-
- if err = ctx.Err(); err != nil {
- return ctx, wrapInternal(err, "aborted because context was done")
- }
-
- if resp.StatusCode != 200 {
- return ctx, errorFromResponse(resp)
- }
-
- d := json.NewDecoder(resp.Body)
- rawRespBody := json.RawMessage{}
- if err := d.Decode(&rawRespBody); err != nil {
- return ctx, wrapInternal(err, "failed to unmarshal json response")
- }
- unmarshaler := protojson.UnmarshalOptions{DiscardUnknown: true}
- if err = unmarshaler.Unmarshal(rawRespBody, out); err != nil {
- return ctx, wrapInternal(err, "failed to unmarshal json response")
- }
- if err = ctx.Err(); err != nil {
- return ctx, wrapInternal(err, "aborted because context was done")
- }
- return ctx, nil
-}
-
-// Call twirp.ServerHooks.RequestReceived if the hook is available
-func callRequestReceived(ctx context.Context, h *twirp.ServerHooks) (context.Context, error) {
- if h == nil || h.RequestReceived == nil {
- return ctx, nil
- }
- return h.RequestReceived(ctx)
-}
-
-// Call twirp.ServerHooks.RequestRouted if the hook is available
-func callRequestRouted(ctx context.Context, h *twirp.ServerHooks) (context.Context, error) {
- if h == nil || h.RequestRouted == nil {
- return ctx, nil
- }
- return h.RequestRouted(ctx)
-}
-
-// Call twirp.ServerHooks.ResponsePrepared if the hook is available
-func callResponsePrepared(ctx context.Context, h *twirp.ServerHooks) context.Context {
- if h == nil || h.ResponsePrepared == nil {
- return ctx
- }
- return h.ResponsePrepared(ctx)
-}
-
-// Call twirp.ServerHooks.ResponseSent if the hook is available
-func callResponseSent(ctx context.Context, h *twirp.ServerHooks) {
- if h == nil || h.ResponseSent == nil {
- return
- }
- h.ResponseSent(ctx)
-}
-
-// Call twirp.ServerHooks.Error if the hook is available
-func callError(ctx context.Context, h *twirp.ServerHooks, err twirp.Error) context.Context {
- if h == nil || h.Error == nil {
- return ctx
- }
- return h.Error(ctx, err)
-}
-
-func callClientResponseReceived(ctx context.Context, h *twirp.ClientHooks) {
- if h == nil || h.ResponseReceived == nil {
- return
- }
- h.ResponseReceived(ctx)
-}
-
-func callClientRequestPrepared(ctx context.Context, h *twirp.ClientHooks, req *http.Request) (context.Context, error) {
- if h == nil || h.RequestPrepared == nil {
- return ctx, nil
- }
- return h.RequestPrepared(ctx, req)
-}
-
-func callClientError(ctx context.Context, h *twirp.ClientHooks, err twirp.Error) {
- if h == nil || h.Error == nil {
- return
- }
- h.Error(ctx, err)
-}
-
-var twirpFileDescriptor0 = []byte{
- // 196 bytes of a gzipped FileDescriptorProto
- 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x4d, 0x4c, 0xca, 0xcc,
- 0xc9, 0x2c, 0xa9, 0xd4, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x4c, 0x2c, 0x2d, 0xc9, 0xa8,
- 0xd2, 0x2b, 0x2a, 0x48, 0x56, 0x4a, 0xe1, 0xe2, 0x71, 0xcc, 0xc9, 0xc9, 0x2f, 0x0f, 0x4a, 0x2d,
- 0x2c, 0x4d, 0x2d, 0x2e, 0x11, 0x92, 0xe0, 0x62, 0x2f, 0x2e, 0x4d, 0xca, 0x4a, 0x4d, 0x2e, 0x91,
- 0x60, 0x54, 0x60, 0xd4, 0xe0, 0x0c, 0x82, 0x71, 0x85, 0xe4, 0xb8, 0xb8, 0x0a, 0x52, 0x8b, 0x72,
- 0x33, 0x8b, 0x8b, 0x33, 0xf3, 0xf3, 0x24, 0x98, 0xc0, 0x92, 0x48, 0x22, 0x42, 0x52, 0x5c, 0x1c,
- 0x45, 0xa9, 0xc5, 0xf9, 0xa5, 0x45, 0xc9, 0xa9, 0x12, 0xcc, 0x60, 0x59, 0x38, 0x5f, 0x49, 0x85,
- 0x8b, 0x0b, 0x6a, 0x4b, 0x41, 0x4e, 0xa5, 0x90, 0x18, 0x17, 0x5b, 0x51, 0x6a, 0x71, 0x69, 0x0e,
- 0xc4, 0x0a, 0x8e, 0x20, 0x28, 0xcf, 0xc8, 0x8d, 0x8b, 0xdd, 0x11, 0xe2, 0x4e, 0x21, 0x6b, 0x2e,
- 0x76, 0xb0, 0x86, 0xd4, 0x14, 0x21, 0x71, 0x3d, 0xb8, 0x6b, 0xf5, 0x90, 0x9d, 0x2a, 0x25, 0x8a,
- 0x29, 0x51, 0x90, 0x53, 0xa9, 0xc4, 0xe0, 0xc4, 0x19, 0xc5, 0x5e, 0x90, 0x9d, 0xae, 0x5f, 0x54,
- 0x90, 0x9c, 0xc4, 0x06, 0xf6, 0xb0, 0x31, 0x20, 0x00, 0x00, 0xff, 0xff, 0x72, 0x35, 0x46, 0x7c,
- 0x01, 0x01, 0x00, 0x00,
-}
diff --git a/protos/ability.proto b/protos/ability.proto
deleted file mode 100644
index b58f5027..00000000
--- a/protos/ability.proto
+++ /dev/null
@@ -1,19 +0,0 @@
-syntax = "proto3";
-
-package authz.rpc;
-option go_package = "pkg/rpc";
-
-
-service Ability {
- rpc Allowed (AllowRequest) returns (AllowReply) {}
-}
-
-message AllowRequest {
- string subject = 1;
- string permission = 2;
- string resource = 3;
-}
-
-message AllowReply {
- bool result = 1;
-}
diff --git a/src/authorization/authorizer.rs b/src/authorization/authorizer.rs
new file mode 100644
index 00000000..14a7df27
--- /dev/null
+++ b/src/authorization/authorizer.rs
@@ -0,0 +1,5 @@
+use envoy_types::ext_authz::v3::pb::CheckRequest;
+
+pub trait Authorizer: std::fmt::Debug {
+ fn authorize(&self, request: CheckRequest) -> bool;
+}
diff --git a/src/authorization/cedar_authorizer.rs b/src/authorization/cedar_authorizer.rs
new file mode 100644
index 00000000..a877cf87
--- /dev/null
+++ b/src/authorization/cedar_authorizer.rs
@@ -0,0 +1,141 @@
+use super::authorizer::Authorizer;
+use cedar_policy::{
+ Authorizer as CedarAuth, Context, Entities, EntityId, EntityTypeName, EntityUid, PolicySet,
+ Request as CedarRequest, RestrictedExpression,
+};
+use envoy_types::ext_authz::v3::pb::CheckRequest;
+use std::collections::HashMap;
+use std::fs;
+use std::str::FromStr;
+
+#[derive(Debug)]
+pub struct CedarAuthorizer {
+ policies: PolicySet,
+ authorizer: CedarAuth,
+}
+
+impl CedarAuthorizer {
+ pub fn new(policies: cedar_policy::PolicySet) -> CedarAuthorizer {
+ CedarAuthorizer {
+ policies,
+ authorizer: CedarAuth::new(),
+ }
+ }
+
+ pub fn new_from(path: &std::path::Path) -> CedarAuthorizer {
+ Self::new(Self::load_from(path).unwrap_or_else(|_| PolicySet::default()))
+ }
+
+ fn load_from(path: &std::path::Path) -> Result<PolicySet, Box<dyn std::error::Error>> {
+ if !path.exists() || !path.is_dir() {
+ return Ok(PolicySet::default());
+ }
+
+ let mut policies = PolicySet::new();
+
+ for entry in fs::read_dir(path)? {
+ let file_path = entry?.path();
+
+ if let Some(extension) = file_path.extension() {
+ if extension == "cedar" {
+ let content = fs::read_to_string(&file_path)?;
+ let file_policies = PolicySet::from_str(&content)?;
+
+ for policy in file_policies.policies() {
+ policies.add(policy.clone())?;
+ }
+ }
+ }
+ }
+
+ Ok(policies)
+ }
+}
+
+impl Default for CedarAuthorizer {
+ fn default() -> Self {
+ Self::new_from(std::path::Path::new("/etc/authzd"))
+ }
+}
+
+impl Authorizer for CedarAuthorizer {
+ fn authorize(&self, request: CheckRequest) -> bool {
+ let headers = match request
+ .attributes
+ .as_ref()
+ .and_then(|attr| attr.request.as_ref())
+ .and_then(|req| req.http.as_ref())
+ .map(|http| &http.headers)
+ {
+ Some(headers) => headers,
+ None => return false,
+ };
+
+ // Extract authorization token
+ let bearer_token = headers
+ .get("authorization")
+ .and_then(|auth| auth.strip_prefix("Bearer "))
+ .unwrap_or("");
+
+ // Extract request path for static asset checking
+ let path = headers
+ .get(":path")
+ .or_else(|| headers.get("path"))
+ .map_or("", |v| v.as_str());
+
+ // Create Cedar entities and request
+ match self.create_cedar_request(bearer_token, path) {
+ Ok(cedar_request) => {
+ let entities = Entities::empty();
+ let response =
+ self.authorizer
+ .is_authorized(&cedar_request, &self.policies, &entities);
+ matches!(response.decision(), cedar_policy::Decision::Allow)
+ }
+ Err(_) => false,
+ }
+ }
+}
+
+impl CedarAuthorizer {
+ fn create_cedar_request(
+ &self,
+ bearer_token: &str,
+ path: &str,
+ ) -> Result<CedarRequest, Box<dyn std::error::Error>> {
+ // Create principal entity
+ let principal_id = EntityId::from_str("client")?;
+ let principal_type = EntityTypeName::from_str("User")?;
+ let principal = EntityUid::from_type_name_and_id(principal_type, principal_id);
+
+ // Create action entity
+ let action_id = EntityId::from_str("check")?;
+ let action_type = EntityTypeName::from_str("Action")?;
+ let action = EntityUid::from_type_name_and_id(action_type, action_id);
+
+ // Create resource entity
+ let resource_id = EntityId::from_str("resource")?;
+ let resource_type = EntityTypeName::from_str("Resource")?;
+ let resource = EntityUid::from_type_name_and_id(resource_type, resource_id);
+
+ // Create context with bearer token and path
+ let mut context_map = HashMap::new();
+ if !bearer_token.is_empty() {
+ context_map.insert(
+ "bearer_token".to_string(),
+ RestrictedExpression::from_str(&format!("\"{}\"", bearer_token))?,
+ );
+ }
+ if !path.is_empty() {
+ context_map.insert(
+ "path".to_string(),
+ RestrictedExpression::from_str(&format!("\"{}\"", path))?,
+ );
+ }
+
+ let context = Context::from_pairs(context_map.into_iter().collect::<Vec<_>>())?;
+
+ CedarRequest::new(principal, action, resource, context, None)
+ .map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
+ }
+}
diff --git a/src/authorization/check_service.rs b/src/authorization/check_service.rs
new file mode 100644
index 00000000..57f7b5d5
--- /dev/null
+++ b/src/authorization/check_service.rs
@@ -0,0 +1,35 @@
+use envoy_types::ext_authz::v3::CheckResponseExt;
+use envoy_types::ext_authz::v3::pb::{CheckRequest, CheckResponse};
+use std::sync::Arc;
+use tonic::{Request, Response, Status};
+
+use super::authorizer::Authorizer;
+
+#[derive(Debug)]
+pub struct CheckService {
+ authorizer: Arc<dyn Authorizer + Send + Sync>,
+}
+
+impl CheckService {
+ pub fn new(authorizer: Arc<dyn Authorizer + Send + Sync>) -> Self {
+ Self { authorizer }
+ }
+}
+
+#[tonic::async_trait]
+impl envoy_types::ext_authz::v3::pb::Authorization for CheckService {
+ async fn check(
+ &self,
+ request: Request<CheckRequest>,
+ ) -> Result<Response<CheckResponse>, Status> {
+ if self.authorizer.authorize(request.into_inner()) {
+ log::info!("OK");
+ Ok(Response::new(CheckResponse::with_status(Status::ok("OK"))))
+ } else {
+ log::info!("Unauthorized");
+ Ok(Response::new(CheckResponse::with_status(
+ Status::unauthenticated("Unauthorized"),
+ )))
+ }
+ }
+}
diff --git a/src/authorization/mod.rs b/src/authorization/mod.rs
new file mode 100644
index 00000000..d664815b
--- /dev/null
+++ b/src/authorization/mod.rs
@@ -0,0 +1,9 @@
+pub mod authorizer;
+pub mod cedar_authorizer;
+pub mod check_service;
+pub mod server;
+
+pub use authorizer::Authorizer;
+pub use cedar_authorizer::CedarAuthorizer;
+pub use check_service::CheckService;
+pub use server::Server;
diff --git a/src/authorization/server.rs b/src/authorization/server.rs
new file mode 100644
index 00000000..2ad270df
--- /dev/null
+++ b/src/authorization/server.rs
@@ -0,0 +1,50 @@
+use super::cedar_authorizer::CedarAuthorizer;
+use super::check_service::CheckService;
+use envoy_types::ext_authz::v3::pb::AuthorizationServer;
+use std::sync::Arc;
+
+pub struct Server {
+ router: tonic::transport::server::Router,
+}
+
+impl Server {
+ pub fn new() -> Result<Server, Box<dyn std::error::Error>> {
+ let (_health_reporter, health_service) = tonic_health::server::health_reporter();
+ let authorization_service =
+ AuthorizationServer::new(CheckService::new(Arc::new(CedarAuthorizer::default())));
+
+ Ok(Self::new_with(|mut builder| {
+ builder
+ .add_service(authorization_service)
+ .add_service(health_service)
+ }))
+ }
+
+ pub fn new_with<F>(f: F) -> Server
+ where
+ F: FnOnce(tonic::transport::Server) -> tonic::transport::server::Router,
+ {
+ let builder = tonic::transport::Server::builder()
+ .trace_fn(|req| {
+ tracing::info_span!(
+ "request",
+ method = %req.method(),
+ path = %req.uri().path(),
+ headers = ?req.headers(),
+ )
+ })
+ .timeout(std::time::Duration::from_secs(30));
+ let router = f(builder);
+ Server { router }
+ }
+
+ pub async fn serve(self, addr: std::net::SocketAddr) -> Result<(), tonic::transport::Error> {
+ self.router.serve(addr).await
+ }
+}
+
+impl Default for Server {
+ fn default() -> Self {
+ Self::new().unwrap()
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 00000000..3bd8fbd1
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,2 @@
+pub mod authorization;
+pub use authorization::{Authorizer, CedarAuthorizer, CheckService, Server};
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 00000000..8638e14b
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,14 @@
+#[tokio::main]
+async fn main() -> Result<(), Box<dyn std::error::Error>> {
+ tracing_subscriber::fmt().json().init();
+
+ let addr = std::env::var("BIND_ADDR")
+ .unwrap_or_else(|_| "[::1]:50051".to_string())
+ .parse()?;
+
+ log::info!("Listening on... {addr}");
+ let server = authzd::authorization::Server::new()?;
+ server.serve(addr).await?;
+
+ Ok(())
+}
diff --git a/tests/authorization/cedar_authorizer_test.rs b/tests/authorization/cedar_authorizer_test.rs
new file mode 100644
index 00000000..76bf06df
--- /dev/null
+++ b/tests/authorization/cedar_authorizer_test.rs
@@ -0,0 +1,66 @@
+#[cfg(test)]
+mod tests {
+ use crate::support::factory_bot::*;
+ use authzd::Authorizer;
+ use envoy_types::pb::envoy::service::auth::v3::attribute_context::HttpRequest;
+ use std::collections::HashMap;
+
+ #[test]
+ fn test_cedar_authorizer_allows_valid_token() {
+ let request = build_request(|item: &mut HttpRequest| {
+ item.headers = build_with(|item: &mut HashMap<String, String>| {
+ item.insert(
+ String::from("authorization"),
+ String::from("Bearer valid-token"),
+ );
+ });
+ });
+
+ assert!(build_cedar_authorizer().authorize(request));
+ }
+
+ #[test]
+ fn test_cedar_authorizer_denies_invalid_token() {
+ let request = build_request(|item: &mut HttpRequest| {
+ item.headers = build_with(|item: &mut HashMap<String, String>| {
+ item.insert(
+ String::from("authorization"),
+ String::from("Bearer invalid-token"),
+ );
+ });
+ });
+
+ assert!(!build_cedar_authorizer().authorize(request));
+ }
+
+ #[test]
+ fn test_cedar_authorizer_denies_missing_header() {
+ let request = build_request(|item: &mut HttpRequest| {
+ item.headers = HashMap::new();
+ });
+
+ assert!(!build_cedar_authorizer().authorize(request));
+ }
+
+ #[test]
+ fn test_cedar_authorizer_allows_static_assets() {
+ let request = build_request(|item: &mut HttpRequest| {
+ item.headers = build_with(|item: &mut HashMap<String, String>| {
+ item.insert(String::from(":path"), String::from("/public/style.css"));
+ });
+ });
+
+ assert!(build_cedar_authorizer().authorize(request));
+ }
+
+ #[test]
+ fn test_cedar_authorizer_allows_js_assets() {
+ let mut headers = HashMap::new();
+ headers.insert(":path".to_string(), "/app.js".to_string());
+ let request = build_request(|item: &mut HttpRequest| {
+ item.headers = headers;
+ });
+
+ assert!(build_cedar_authorizer().authorize(request));
+ }
+}
diff --git a/tests/authorization/check_service_test.rs b/tests/authorization/check_service_test.rs
new file mode 100644
index 00000000..3a225974
--- /dev/null
+++ b/tests/authorization/check_service_test.rs
@@ -0,0 +1,118 @@
+#[cfg(test)]
+mod tests {
+ use crate::support::factory_bot::*;
+ use authzd::CheckService;
+ use envoy_types::ext_authz::v3::pb::Authorization;
+ use envoy_types::pb::envoy::service::auth::v3::attribute_context::HttpRequest;
+ use std::collections::HashMap;
+ use std::sync::Arc;
+
+ fn subject() -> CheckService {
+ CheckService::new(Arc::new(build_cedar_authorizer()))
+ }
+
+ #[tokio::test]
+ async fn test_check_allows_valid_bearer_token() {
+ let request = tonic::Request::new(build_request(|item: &mut HttpRequest| {
+ item.headers = build_headers(vec![(
+ "authorization".to_string(),
+ format!("Bearer {}", String::from("valid-token")),
+ )])
+ }));
+
+ let response = subject().check(request).await;
+ assert!(response.is_ok());
+
+ let check_response = response.unwrap().into_inner();
+ assert!(check_response.status.is_some());
+
+ let status = check_response.status.unwrap();
+ assert_eq!(status.code, tonic::Code::Ok as i32);
+ }
+
+ #[tokio::test]
+ async fn test_check_denies_invalid_bearer_token() {
+ let request = tonic::Request::new(build_request(|item: &mut HttpRequest| {
+ item.headers = HashMap::new();
+ }));
+
+ let response = subject().check(request).await;
+ assert!(response.is_ok());
+
+ let check_response = response.unwrap().into_inner();
+ assert!(check_response.status.is_some());
+
+ let status = check_response.status.unwrap();
+ assert_eq!(status.code, tonic::Code::Unauthenticated as i32);
+ }
+
+ #[tokio::test]
+ async fn test_static_assets() {
+ let static_paths = vec![
+ "app.js",
+ "favicon.ico",
+ "image.jpg",
+ "index.html",
+ "logo.png",
+ "style.css",
+ ];
+
+ for path in static_paths {
+ let request = tonic::Request::new(build_request(|http| {
+ http.headers = build_headers(vec![(":path".to_string(), path.to_string())]);
+ }));
+
+ let response = subject().check(request).await;
+ assert!(response.is_ok());
+
+ let check_response = response.unwrap().into_inner();
+ assert!(check_response.status.is_some());
+
+ let status = check_response.status.unwrap();
+ assert_eq!(status.code, tonic::Code::Ok as i32);
+ }
+ }
+
+ #[tokio::test]
+ async fn test_no_headers() {
+ let request = tonic::Request::new(build_request(|_http| {}));
+
+ let response = subject().check(request).await;
+ assert!(response.is_ok());
+
+ let check_response = response.unwrap().into_inner();
+ assert!(check_response.status.is_some());
+
+ let status = check_response.status.unwrap();
+ assert_eq!(status.code, tonic::Code::Unauthenticated as i32);
+ }
+
+ #[tokio::test]
+ async fn test_table() {
+ let test_cases = vec![
+ ("Bearer valid-token", true),
+ ("Bearer invalid-token", false),
+ ("Basic valid-token", false),
+ ("", false),
+ ];
+
+ for (auth_value, should_succeed) in test_cases {
+ let request = tonic::Request::new(build_request(|item: &mut HttpRequest| {
+ item.headers =
+ build_headers(vec![("authorization".to_string(), auth_value.to_string())]);
+ }));
+
+ let response = subject().check(request).await;
+ assert!(response.is_ok());
+
+ let check_response = response.unwrap().into_inner();
+ let status = check_response.status.unwrap();
+
+ if should_succeed {
+ assert_eq!(status.code, tonic::Code::Ok as i32);
+ } else {
+ assert_eq!(status.code, tonic::Code::Unauthenticated as i32);
+ }
+ }
+ }
+}
diff --git a/tests/authorization/mod.rs b/tests/authorization/mod.rs
new file mode 100644
index 00000000..675247d4
--- /dev/null
+++ b/tests/authorization/mod.rs
@@ -0,0 +1,3 @@
+mod cedar_authorizer_test;
+mod check_service_test;
+mod server_test;
diff --git a/tests/authorization/server_test.rs b/tests/authorization/server_test.rs
new file mode 100644
index 00000000..fe8c8a73
--- /dev/null
+++ b/tests/authorization/server_test.rs
@@ -0,0 +1,48 @@
+#[cfg(test)]
+mod tests {
+ use crate::support::factory_bot::*;
+ use std::net::SocketAddr;
+ use tokio::net::TcpListener;
+
+ async fn available_port() -> SocketAddr {
+ let listener = TcpListener::bind("127.0.0.1:0")
+ .await
+ .expect("Failed to bind to random port");
+ let addr = listener.local_addr().expect("Failed to get local address");
+ drop(listener);
+ addr
+ }
+
+ async fn start_server() -> (SocketAddr, tokio::task::JoinHandle<()>) {
+ let addr = available_port().await;
+ let server = authzd::authorization::Server::default();
+
+ let handle = tokio::spawn(async move {
+ server.serve(addr).await.expect("Failed to start server");
+ });
+
+ tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
+
+ (addr, handle)
+ }
+
+ #[tokio::test]
+ async fn test_health_check_service() {
+ let (addr, server) = start_server().await;
+ let mut client =
+ build_rpc_client(addr, tonic_health::pb::health_client::HealthClient::new).await;
+
+ let request = tonic::Request::new(tonic_health::pb::HealthCheckRequest {
+ service: String::new(),
+ });
+ let response = client.check(request).await;
+
+ assert!(response.is_ok());
+ assert_eq!(
+ response.unwrap().into_inner().status(),
+ tonic_health::pb::health_check_response::ServingStatus::Serving
+ );
+
+ server.abort();
+ }
+}
diff --git a/tests/integration_test.rs b/tests/integration_test.rs
new file mode 100644
index 00000000..c17d8e65
--- /dev/null
+++ b/tests/integration_test.rs
@@ -0,0 +1,2 @@
+mod authorization;
+mod support;
diff --git a/tests/support/factory_bot.rs b/tests/support/factory_bot.rs
new file mode 100644
index 00000000..15c6f1f3
--- /dev/null
+++ b/tests/support/factory_bot.rs
@@ -0,0 +1,58 @@
+use envoy_types::ext_authz::v3::pb::CheckRequest;
+use envoy_types::pb::envoy::service::auth::v3::AttributeContext;
+use envoy_types::pb::envoy::service::auth::v3::attribute_context::{HttpRequest, Request};
+use std::collections::HashMap;
+use std::net::SocketAddr;
+use tonic::transport::Channel;
+
+#[allow(dead_code)]
+pub fn build<T: Default>() -> T {
+ return please::build();
+}
+
+pub fn build_with<T, F>(initializer: F) -> T
+where
+ T: Default,
+ F: std::ops::FnOnce(&mut T),
+{
+ return please::build_with(initializer);
+}
+
+pub fn build_request(f: impl std::ops::FnOnce(&mut HttpRequest)) -> CheckRequest {
+ build_with(|item: &mut CheckRequest| {
+ item.attributes = Some(please::build_with(|item: &mut AttributeContext| {
+ item.request = Some(please::build_with(|item: &mut Request| {
+ item.http = Some(please::build_with(|item: &mut HttpRequest| f(item)));
+ }));
+ }));
+ })
+}
+
+pub fn build_headers(headers: Vec<(String, String)>) -> HashMap<String, String> {
+ return build_with(|item: &mut HashMap<String, String>| {
+ for (key, value) in headers {
+ item.insert(key, value);
+ }
+ });
+}
+
+pub fn build_cedar_authorizer() -> authzd::CedarAuthorizer {
+ let realpath = std::fs::canonicalize("./etc/authzd").unwrap();
+ let path = realpath.as_path();
+ authzd::CedarAuthorizer::new_from(path)
+}
+
+pub async fn build_channel(addr: SocketAddr) -> Channel {
+ Channel::from_shared(format!("http://{}", addr))
+ .expect("Failed to create channel")
+ .connect()
+ .await
+ .expect("Failed to connect to server")
+}
+
+pub async fn build_rpc_client<T, F>(addr: SocketAddr, f: F) -> T
+where
+ F: FnOnce(Channel) -> T,
+{
+ f(build_channel(addr).await)
+}
diff --git a/tests/support/mod.rs b/tests/support/mod.rs
new file mode 100644
index 00000000..5e2a6d78
--- /dev/null
+++ b/tests/support/mod.rs
@@ -0,0 +1 @@
+pub mod factory_bot;
diff --git a/tmp/cache/.keep b/tmp/cache/.keep
deleted file mode 100644
index e69de29b..00000000
--- a/tmp/cache/.keep
+++ /dev/null
diff --git a/tmp/pids/.keep b/tmp/pids/.keep
deleted file mode 100644
index e69de29b..00000000
--- a/tmp/pids/.keep
+++ /dev/null