Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52a5c4141f | ||
|
|
c9ba27c333 | ||
|
|
46f6e2c58b | ||
|
|
d1f47e5a22 | ||
|
|
a9fb092834 | ||
|
|
3a21a68792 |
Generated
+519
-1
@@ -26,6 +26,16 @@ dependencies = [
|
||||
"pom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aead"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
|
||||
dependencies = [
|
||||
"crypto-common 0.1.7",
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aes"
|
||||
version = "0.8.4"
|
||||
@@ -37,6 +47,20 @@ dependencies = [
|
||||
"cpufeatures 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aes-gcm"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
|
||||
dependencies = [
|
||||
"aead",
|
||||
"aes",
|
||||
"cipher",
|
||||
"ctr",
|
||||
"ghash",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "agen"
|
||||
version = "0.2.1"
|
||||
@@ -326,6 +350,12 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base16ct"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.21.7"
|
||||
@@ -338,6 +368,12 @@ version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||
|
||||
[[package]]
|
||||
name = "base64urlsafedata"
|
||||
version = "0.5.5"
|
||||
@@ -349,6 +385,17 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bcrypt-pbkdf"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6aeac2e1fe888769f34f05ac343bbef98b14d1ffb292ab69d4608b3abc86f2a2"
|
||||
dependencies = [
|
||||
"blowfish",
|
||||
"pbkdf2",
|
||||
"sha2 0.10.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-set"
|
||||
version = "0.5.3"
|
||||
@@ -403,6 +450,16 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blowfish"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "1.12.1"
|
||||
@@ -435,6 +492,12 @@ version = "1.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.11.1"
|
||||
@@ -495,6 +558,17 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher",
|
||||
"cpufeatures 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.44"
|
||||
@@ -654,6 +728,12 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.10.2"
|
||||
@@ -937,6 +1017,18 @@ version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-bigint"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"rand_core 0.6.4",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
@@ -966,6 +1058,41 @@ dependencies = [
|
||||
"phf 0.11.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctr"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
|
||||
dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek"
|
||||
version = "4.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.17",
|
||||
"curve25519-dalek-derive",
|
||||
"digest 0.10.7",
|
||||
"fiat-crypto",
|
||||
"rustc_version",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek-derive"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.23.0"
|
||||
@@ -1056,6 +1183,16 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4"
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
||||
dependencies = [
|
||||
"const-oid 0.9.6",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der-parser"
|
||||
version = "9.0.0"
|
||||
@@ -1114,7 +1251,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer 0.10.4",
|
||||
"const-oid 0.9.6",
|
||||
"crypto-common 0.1.7",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1124,7 +1263,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c"
|
||||
dependencies = [
|
||||
"block-buffer 0.12.0",
|
||||
"const-oid",
|
||||
"const-oid 0.10.2",
|
||||
"crypto-common 0.2.1",
|
||||
]
|
||||
|
||||
@@ -1175,12 +1314,66 @@ dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ecdsa"
|
||||
version = "0.16.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
|
||||
dependencies = [
|
||||
"der",
|
||||
"digest 0.10.7",
|
||||
"elliptic-curve",
|
||||
"rfc6979",
|
||||
"signature",
|
||||
"spki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ed25519"
|
||||
version = "2.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
|
||||
dependencies = [
|
||||
"signature",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ed25519-dalek"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
|
||||
dependencies = [
|
||||
"curve25519-dalek",
|
||||
"ed25519",
|
||||
"sha2 0.10.9",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||
|
||||
[[package]]
|
||||
name = "elliptic-curve"
|
||||
version = "0.13.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
|
||||
dependencies = [
|
||||
"base16ct",
|
||||
"crypto-bigint",
|
||||
"digest 0.10.7",
|
||||
"ff",
|
||||
"generic-array",
|
||||
"group",
|
||||
"pkcs8",
|
||||
"rand_core 0.6.4",
|
||||
"sec1",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "embedded-io"
|
||||
version = "0.4.0"
|
||||
@@ -1284,6 +1477,22 @@ version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "ff"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
|
||||
dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fiat-crypto"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
|
||||
|
||||
[[package]]
|
||||
name = "filedescriptor"
|
||||
version = "0.8.3"
|
||||
@@ -1526,6 +1735,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1568,6 +1778,16 @@ dependencies = [
|
||||
"wasip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ghash"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
|
||||
dependencies = [
|
||||
"opaque-debug",
|
||||
"polyval",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.33.0"
|
||||
@@ -1636,6 +1856,17 @@ dependencies = [
|
||||
"memmap2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "group"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
|
||||
dependencies = [
|
||||
"ff",
|
||||
"rand_core 0.6.4",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.13"
|
||||
@@ -1724,6 +1955,15 @@ version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||
dependencies = [
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "html5ever"
|
||||
version = "0.26.0"
|
||||
@@ -2212,6 +2452,9 @@ name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
dependencies = [
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "leb128fmt"
|
||||
@@ -2672,6 +2915,22 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint-dig"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"libm",
|
||||
"num-integer",
|
||||
"num-iter",
|
||||
"num-traits",
|
||||
"rand 0.8.5",
|
||||
"smallvec",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.2.1"
|
||||
@@ -2698,6 +2957,16 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-iter"
|
||||
version = "0.1.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
@@ -2705,6 +2974,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2765,6 +3035,12 @@ version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.76"
|
||||
@@ -2818,6 +3094,44 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "p256"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
|
||||
dependencies = [
|
||||
"ecdsa",
|
||||
"elliptic-curve",
|
||||
"primeorder",
|
||||
"sha2 0.10.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "p384"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6"
|
||||
dependencies = [
|
||||
"ecdsa",
|
||||
"elliptic-curve",
|
||||
"primeorder",
|
||||
"sha2 0.10.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "p521"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2"
|
||||
dependencies = [
|
||||
"base16ct",
|
||||
"ecdsa",
|
||||
"elliptic-curve",
|
||||
"primeorder",
|
||||
"rand_core 0.6.4",
|
||||
"sha2 0.10.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
@@ -2847,6 +3161,15 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
|
||||
|
||||
[[package]]
|
||||
name = "pbkdf2"
|
||||
version = "0.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
|
||||
dependencies = [
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pdf-extract"
|
||||
version = "0.10.0"
|
||||
@@ -2864,6 +3187,15 @@ dependencies = [
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pem-rfc7468"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
@@ -3009,6 +3341,27 @@ version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "pkcs1"
|
||||
version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
|
||||
dependencies = [
|
||||
"der",
|
||||
"pkcs8",
|
||||
"spki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkcs8"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
|
||||
dependencies = [
|
||||
"der",
|
||||
"spki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.32"
|
||||
@@ -3021,6 +3374,29 @@ version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
||||
|
||||
[[package]]
|
||||
name = "poly1305"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
|
||||
dependencies = [
|
||||
"cpufeatures 0.2.17",
|
||||
"opaque-debug",
|
||||
"universal-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "polyval"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.17",
|
||||
"opaque-debug",
|
||||
"universal-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pom"
|
||||
version = "1.1.0"
|
||||
@@ -3101,6 +3477,15 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "primeorder"
|
||||
version = "0.13.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
|
||||
dependencies = [
|
||||
"elliptic-curve",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
@@ -3525,6 +3910,16 @@ dependencies = [
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rfc6979"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
|
||||
dependencies = [
|
||||
"hmac",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
@@ -3539,6 +3934,27 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rsa"
|
||||
version = "0.9.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
|
||||
dependencies = [
|
||||
"const-oid 0.9.6",
|
||||
"digest 0.10.7",
|
||||
"num-bigint-dig",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"pkcs1",
|
||||
"pkcs8",
|
||||
"rand_core 0.6.4",
|
||||
"sha2 0.10.9",
|
||||
"signature",
|
||||
"spki",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.37.0"
|
||||
@@ -3745,6 +4161,20 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "sec1"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
|
||||
dependencies = [
|
||||
"base16ct",
|
||||
"der",
|
||||
"generic-array",
|
||||
"pkcs8",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "secrets"
|
||||
version = "0.1.0"
|
||||
@@ -4060,6 +4490,16 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signature"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||
dependencies = [
|
||||
"digest 0.10.7",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.9"
|
||||
@@ -4103,6 +4543,72 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e"
|
||||
|
||||
[[package]]
|
||||
name = "spki"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"der",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ssh-cipher"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
"cbc",
|
||||
"chacha20",
|
||||
"cipher",
|
||||
"ctr",
|
||||
"poly1305",
|
||||
"ssh-encoding",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ssh-encoding"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"pem-rfc7468",
|
||||
"sha2 0.10.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ssh-key"
|
||||
version = "0.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b86f5297f0f04d08cabaa0f6bff7cb6aec4d9c3b49d87990d63da9d9156a8c3"
|
||||
dependencies = [
|
||||
"bcrypt-pbkdf",
|
||||
"ed25519-dalek",
|
||||
"p256",
|
||||
"p384",
|
||||
"p521",
|
||||
"rand_core 0.6.4",
|
||||
"rsa",
|
||||
"sec1",
|
||||
"sha2 0.10.9",
|
||||
"signature",
|
||||
"ssh-cipher",
|
||||
"ssh-encoding",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.1"
|
||||
@@ -4934,6 +5440,16 @@ version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "universal-hash"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
|
||||
dependencies = [
|
||||
"crypto-common 0.1.7",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unsafe-libyaml"
|
||||
version = "0.2.11"
|
||||
@@ -6259,11 +6775,13 @@ dependencies = [
|
||||
"project-record",
|
||||
"protocol",
|
||||
"reqwest",
|
||||
"ring",
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"sha2 0.11.0",
|
||||
"ssh-key",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"ticket",
|
||||
|
||||
@@ -115,6 +115,7 @@ tar = "0.4"
|
||||
rusqlite = { version = "0.37", features = ["backup", "bundled"] }
|
||||
ring = "0.17.14"
|
||||
sha2 = "0.11"
|
||||
ssh-key = { version = "0.6.7", features = ["ed25519", "encryption"] }
|
||||
tempfile = "3.27"
|
||||
thiserror = "2.0"
|
||||
tokio = "1.52"
|
||||
|
||||
@@ -373,6 +373,116 @@ pub struct UpdateWorkspaceMemorySettingsRequest {
|
||||
pub language: String,
|
||||
}
|
||||
|
||||
/// Public metadata for one Workspace-scoped Repository SSH credential.
|
||||
///
|
||||
/// Secret references and secret material are deliberately not part of this DTO.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct RepositorySshCredential {
|
||||
pub credential_id: String,
|
||||
pub workspace_id: String,
|
||||
pub name: String,
|
||||
pub public_key_algorithm: String,
|
||||
pub public_key_fingerprint: String,
|
||||
pub current_revision: u64,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
pub rotated_at: Option<String>,
|
||||
#[serde(default)]
|
||||
pub referenced_repositories: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct CreateRepositorySshCredentialRequest {
|
||||
pub operation_id: String,
|
||||
pub credential_id: String,
|
||||
pub name: String,
|
||||
pub private_key: String,
|
||||
#[serde(default)]
|
||||
pub passphrase: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct RotateRepositorySshCredentialRequest {
|
||||
pub operation_id: String,
|
||||
pub expected_revision: u64,
|
||||
pub private_key: String,
|
||||
#[serde(default)]
|
||||
pub passphrase: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct DeleteRepositorySshCredentialRequest {
|
||||
pub operation_id: String,
|
||||
pub expected_revision: u64,
|
||||
}
|
||||
|
||||
/// Public metadata for an explicitly pinned SSH host key.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct RepositorySshHostTrust {
|
||||
pub host_trust_id: String,
|
||||
pub workspace_id: String,
|
||||
pub hostname: String,
|
||||
pub port: u16,
|
||||
pub key_algorithm: String,
|
||||
pub host_key: String,
|
||||
pub fingerprint: String,
|
||||
pub current_revision: u64,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
#[serde(default)]
|
||||
pub referenced_repositories: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct PutRepositorySshHostTrustRequest {
|
||||
pub operation_id: String,
|
||||
pub host_trust_id: String,
|
||||
pub hostname: String,
|
||||
pub port: u16,
|
||||
pub host_key: String,
|
||||
#[serde(default)]
|
||||
pub expected_revision: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct DeleteRepositorySshHostTrustRequest {
|
||||
pub operation_id: String,
|
||||
pub expected_revision: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum RepositoryAccessMode {
|
||||
ReadOnly,
|
||||
ReadWrite,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct RepositorySshAccessBinding {
|
||||
pub repository_id: String,
|
||||
pub credential_id: String,
|
||||
pub host_trust_id: String,
|
||||
pub access: RepositoryAccessMode,
|
||||
}
|
||||
|
||||
/// Secret-free active Repository access projection consumed by later Runtime work.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct RepositoryAccessProjection {
|
||||
pub workspace_id: String,
|
||||
pub config_revision: u64,
|
||||
pub projection_digest: String,
|
||||
pub bindings: Vec<RepositorySshAccessBinding>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -25,11 +25,13 @@ manifest.workspace = true
|
||||
protocol = { workspace = true }
|
||||
project-record.workspace = true
|
||||
reqwest = { version = "0.13", default-features = false, features = ["blocking", "json", "native-tls"] }
|
||||
ring.workspace = true
|
||||
rusqlite.workspace = true
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
serde_yaml.workspace = true
|
||||
sha2.workspace = true
|
||||
ssh-key.workspace = true
|
||||
thiserror.workspace = true
|
||||
ticket.workspace = true
|
||||
memory.workspace = true
|
||||
|
||||
@@ -20,6 +20,7 @@ pub mod records;
|
||||
#[cfg(feature = "typescript")]
|
||||
pub use records::ticket_api_typescript;
|
||||
pub mod repositories;
|
||||
pub mod repository_access;
|
||||
pub mod repository_source;
|
||||
pub mod resource_broker;
|
||||
pub mod retention;
|
||||
@@ -116,6 +117,8 @@ pub enum Error {
|
||||
TicketAssignmentConflict(String),
|
||||
#[error("Workdir attachment conflict: {0}")]
|
||||
WorkdirAttachmentConflict(String),
|
||||
#[error("Workspace permission denied: {0}")]
|
||||
WorkspacePermissionDenied(String),
|
||||
#[error("Workspace config update conflict: {0}")]
|
||||
WorkspaceConfigConflict(String),
|
||||
#[error("Registry inconsistency: {0}")]
|
||||
|
||||
@@ -1122,7 +1122,7 @@ mod tests {
|
||||
assert_eq!(repositories.len(), 1);
|
||||
assert_eq!(repositories[0].repository_id, "main");
|
||||
assert_eq!(
|
||||
repositories[0].uri,
|
||||
repositories[0].source.uri,
|
||||
temp.path().canonicalize().unwrap().display().to_string()
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@ use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Mutex, Weak};
|
||||
|
||||
use axum::extract::ws::{Message as WsMessage, WebSocket, WebSocketUpgrade};
|
||||
use axum::extract::{Path as AxumPath, Query, Request, State};
|
||||
use axum::extract::{Extension, Path as AxumPath, Query, Request, State};
|
||||
use axum::http::header::{CONTENT_TYPE, ETAG, IF_NONE_MATCH, LOCATION, ORIGIN, SET_COOKIE};
|
||||
use axum::http::{HeaderMap, Method, StatusCode, Uri};
|
||||
use axum::middleware::{self, Next};
|
||||
@@ -58,8 +58,12 @@ use worker::feature::builtin::{WorkerObservationSubject, WorkerObservationSubjec
|
||||
use worker_runtime::resource::{BackendResourceError, BackendResourceFetchRequest};
|
||||
use worker_runtime::worker_backend::{ProfileRuntimeWorkerFactory, WorkerRuntimeExecutionBackend};
|
||||
use workspace_api::{
|
||||
ObjectiveCreateRequest, ObjectiveEditRequest, ObjectiveLinkTicketRequest,
|
||||
ObjectiveStateRequest, TICKET_ORCHESTRATION_PLANS_QUERY_PATH, TICKET_RELATIONS_QUERY_PATH,
|
||||
CreateRepositorySshCredentialRequest, DeleteRepositorySshCredentialRequest,
|
||||
DeleteRepositorySshHostTrustRequest, ObjectiveCreateRequest, ObjectiveEditRequest,
|
||||
ObjectiveLinkTicketRequest, ObjectiveStateRequest, PutRepositorySshHostTrustRequest,
|
||||
RepositoryAccessProjection, RepositorySshCredential, RepositorySshHostTrust,
|
||||
RotateRepositorySshCredentialRequest, TICKET_ORCHESTRATION_PLANS_QUERY_PATH,
|
||||
TICKET_RELATIONS_QUERY_PATH,
|
||||
};
|
||||
|
||||
use crate::auth::{
|
||||
@@ -109,6 +113,10 @@ use crate::repositories::{
|
||||
ConfiguredRepository, RepositoryListProjection, RepositoryLogRead, RepositoryLookupError,
|
||||
RepositoryRegistryReader, RepositorySummary,
|
||||
};
|
||||
use crate::repository_access::{
|
||||
RepositoryAccessConfigSchemaProvider, RepositorySecretService,
|
||||
project_repository_access_candidate, project_repository_access_state,
|
||||
};
|
||||
use crate::resource_broker::BackendResourceBroker;
|
||||
use crate::runtime_settings::RuntimeConfigSchemaProvider;
|
||||
use crate::runtime_subscription::RuntimeSubscriptionBroker;
|
||||
@@ -342,6 +350,7 @@ pub struct WorkspaceApi {
|
||||
pub(crate) config: ServerConfig,
|
||||
pub(crate) store: Arc<dyn ControlPlaneStore>,
|
||||
config_store: Arc<crate::SqliteWorkspaceStore>,
|
||||
repository_secrets: Arc<RepositorySecretService>,
|
||||
config_schema_registry: crate::config_source::WorkspaceConfigSchemaRegistry,
|
||||
prompt_projection_cache: crate::prompt_settings::WorkspacePromptProjectionCache,
|
||||
authority: SqliteWorkspaceAuthority,
|
||||
@@ -1053,6 +1062,7 @@ async fn authorize_workspace_api_request(
|
||||
return StatusCode::FORBIDDEN.into_response();
|
||||
}
|
||||
}
|
||||
request.extensions_mut().insert(actor);
|
||||
next.run(request).await
|
||||
}
|
||||
|
||||
@@ -1343,12 +1353,17 @@ impl WorkspaceApi {
|
||||
let config_store = Arc::new(crate::SqliteWorkspaceStore::open(
|
||||
config.database_path.clone(),
|
||||
)?);
|
||||
let repository_secrets = Arc::new(RepositorySecretService::open(
|
||||
config_store.clone(),
|
||||
&config.database_path,
|
||||
)?);
|
||||
let config_schema_registry = crate::config_source::WorkspaceConfigSchemaRegistry::default()
|
||||
.with_provider(Arc::new(
|
||||
crate::profile_settings::ProfileConfigSchemaProvider,
|
||||
))
|
||||
.with_provider(Arc::new(crate::prompt_settings::PromptConfigSchemaProvider))
|
||||
.with_provider(Arc::new(RuntimeConfigSchemaProvider))
|
||||
.with_provider(Arc::new(RepositoryAccessConfigSchemaProvider))
|
||||
.with_provider(Arc::new(skills::SkillConfigSchemaProvider));
|
||||
config_store.ensure_workspace_config_materialized_with_schema(
|
||||
&config.workspace_id,
|
||||
@@ -1357,6 +1372,7 @@ impl WorkspaceApi {
|
||||
)?;
|
||||
let api = Self {
|
||||
config_store,
|
||||
repository_secrets,
|
||||
config_schema_registry,
|
||||
prompt_projection_cache:
|
||||
crate::prompt_settings::WorkspacePromptProjectionCache::default(),
|
||||
@@ -1863,6 +1879,34 @@ fn build_inner_router(api: WorkspaceApi) -> Router {
|
||||
get(scoped_get_workspace_memory_settings)
|
||||
.put(scoped_update_workspace_memory_settings),
|
||||
)
|
||||
.route(
|
||||
"/api/w/{workspace_id}/settings/repository-access",
|
||||
get(scoped_get_repository_access_projection),
|
||||
)
|
||||
.route(
|
||||
"/api/w/{workspace_id}/settings/repository-access/credentials",
|
||||
get(scoped_list_repository_ssh_credentials)
|
||||
.post(scoped_create_repository_ssh_credential),
|
||||
)
|
||||
.route(
|
||||
"/api/w/{workspace_id}/settings/repository-access/credentials/{credential_id}",
|
||||
get(scoped_get_repository_ssh_credential)
|
||||
.delete(scoped_delete_repository_ssh_credential),
|
||||
)
|
||||
.route(
|
||||
"/api/w/{workspace_id}/settings/repository-access/credentials/{credential_id}/rotate",
|
||||
post(scoped_rotate_repository_ssh_credential),
|
||||
)
|
||||
.route(
|
||||
"/api/w/{workspace_id}/settings/repository-access/host-trusts",
|
||||
get(scoped_list_repository_ssh_host_trusts)
|
||||
.post(scoped_put_repository_ssh_host_trust),
|
||||
)
|
||||
.route(
|
||||
"/api/w/{workspace_id}/settings/repository-access/host-trusts/{host_trust_id}",
|
||||
get(scoped_get_repository_ssh_host_trust)
|
||||
.delete(scoped_delete_repository_ssh_host_trust),
|
||||
)
|
||||
.route(
|
||||
"/api/w/{workspace_id}/config/source-tree",
|
||||
get(scoped_get_workspace_config_tree),
|
||||
@@ -2986,6 +3030,18 @@ struct ScopedRepositoryPath {
|
||||
repository_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ScopedRepositoryCredentialPath {
|
||||
workspace_id: String,
|
||||
credential_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ScopedRepositoryHostTrustPath {
|
||||
workspace_id: String,
|
||||
host_trust_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ScopedProfileArchivePath {
|
||||
workspace_id: String,
|
||||
@@ -3274,6 +3330,194 @@ async fn scoped_update_workspace_memory_settings(
|
||||
}))
|
||||
}
|
||||
|
||||
async fn require_manage_repository_secrets(
|
||||
api: &WorkspaceApi,
|
||||
workspace_id: &str,
|
||||
actor: &RequestActor,
|
||||
) -> ApiResult<()> {
|
||||
validate_workspace_scope(api, workspace_id)?;
|
||||
let workspace = api
|
||||
.store
|
||||
.get_workspace(workspace_id)
|
||||
.await?
|
||||
.ok_or(Error::WorkspaceIdMismatch)?;
|
||||
if workspace.owner_account_id.as_deref() != Some(actor.account_id.as_str()) {
|
||||
return Err(Error::WorkspacePermissionDenied(
|
||||
"ManageSecrets requires the Workspace owner account".to_string(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn active_repository_access_projection(
|
||||
api: &WorkspaceApi,
|
||||
workspace_id: &str,
|
||||
) -> ApiResult<RepositoryAccessProjection> {
|
||||
let state = api
|
||||
.config_store
|
||||
.load_workspace_config(workspace_id)?
|
||||
.ok_or_else(|| Error::InvalidRecordId("virtual config source tree".into()))?;
|
||||
Ok(project_repository_access_state(
|
||||
&*api.store,
|
||||
&api.repository_secrets,
|
||||
workspace_id,
|
||||
&state,
|
||||
)?)
|
||||
}
|
||||
|
||||
async fn scoped_get_repository_access_projection(
|
||||
State(api): State<WorkspaceApi>,
|
||||
AxumPath(path): AxumPath<ScopedWorkspacePath>,
|
||||
Extension(actor): Extension<RequestActor>,
|
||||
) -> ApiResult<Json<RepositoryAccessProjection>> {
|
||||
require_manage_repository_secrets(&api, &path.workspace_id, &actor).await?;
|
||||
Ok(Json(active_repository_access_projection(
|
||||
&api,
|
||||
&path.workspace_id,
|
||||
)?))
|
||||
}
|
||||
|
||||
async fn scoped_list_repository_ssh_credentials(
|
||||
State(api): State<WorkspaceApi>,
|
||||
AxumPath(path): AxumPath<ScopedWorkspacePath>,
|
||||
Extension(actor): Extension<RequestActor>,
|
||||
) -> ApiResult<Json<Vec<RepositorySshCredential>>> {
|
||||
require_manage_repository_secrets(&api, &path.workspace_id, &actor).await?;
|
||||
let projection = active_repository_access_projection(&api, &path.workspace_id)?;
|
||||
Ok(Json(
|
||||
api.repository_secrets
|
||||
.list_credentials(&path.workspace_id, &projection)?,
|
||||
))
|
||||
}
|
||||
|
||||
async fn scoped_get_repository_ssh_credential(
|
||||
State(api): State<WorkspaceApi>,
|
||||
AxumPath(path): AxumPath<ScopedRepositoryCredentialPath>,
|
||||
Extension(actor): Extension<RequestActor>,
|
||||
) -> ApiResult<Json<RepositorySshCredential>> {
|
||||
require_manage_repository_secrets(&api, &path.workspace_id, &actor).await?;
|
||||
let projection = active_repository_access_projection(&api, &path.workspace_id)?;
|
||||
let credential = api
|
||||
.repository_secrets
|
||||
.list_credentials(&path.workspace_id, &projection)?
|
||||
.into_iter()
|
||||
.find(|credential| credential.credential_id == path.credential_id)
|
||||
.ok_or_else(|| Error::InvalidRecordId(path.credential_id.clone()))?;
|
||||
Ok(Json(credential))
|
||||
}
|
||||
|
||||
async fn scoped_create_repository_ssh_credential(
|
||||
State(api): State<WorkspaceApi>,
|
||||
AxumPath(path): AxumPath<ScopedWorkspacePath>,
|
||||
Extension(actor): Extension<RequestActor>,
|
||||
Json(request): Json<CreateRepositorySshCredentialRequest>,
|
||||
) -> ApiResult<(StatusCode, Json<RepositorySshCredential>)> {
|
||||
require_manage_repository_secrets(&api, &path.workspace_id, &actor).await?;
|
||||
let credential =
|
||||
api.repository_secrets
|
||||
.create_credential(&path.workspace_id, request, &actor.account_id)?;
|
||||
Ok((StatusCode::CREATED, Json(credential)))
|
||||
}
|
||||
|
||||
async fn scoped_rotate_repository_ssh_credential(
|
||||
State(api): State<WorkspaceApi>,
|
||||
AxumPath(path): AxumPath<ScopedRepositoryCredentialPath>,
|
||||
Extension(actor): Extension<RequestActor>,
|
||||
Json(request): Json<RotateRepositorySshCredentialRequest>,
|
||||
) -> ApiResult<Json<RepositorySshCredential>> {
|
||||
require_manage_repository_secrets(&api, &path.workspace_id, &actor).await?;
|
||||
Ok(Json(api.repository_secrets.rotate_credential(
|
||||
&path.workspace_id,
|
||||
&path.credential_id,
|
||||
request,
|
||||
&actor.account_id,
|
||||
)?))
|
||||
}
|
||||
|
||||
async fn scoped_delete_repository_ssh_credential(
|
||||
State(api): State<WorkspaceApi>,
|
||||
AxumPath(path): AxumPath<ScopedRepositoryCredentialPath>,
|
||||
Extension(actor): Extension<RequestActor>,
|
||||
Json(request): Json<DeleteRepositorySshCredentialRequest>,
|
||||
) -> ApiResult<StatusCode> {
|
||||
require_manage_repository_secrets(&api, &path.workspace_id, &actor).await?;
|
||||
let projection = active_repository_access_projection(&api, &path.workspace_id)?;
|
||||
api.repository_secrets.delete_credential(
|
||||
&path.workspace_id,
|
||||
&path.credential_id,
|
||||
request,
|
||||
&actor.account_id,
|
||||
&projection,
|
||||
)?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
async fn scoped_list_repository_ssh_host_trusts(
|
||||
State(api): State<WorkspaceApi>,
|
||||
AxumPath(path): AxumPath<ScopedWorkspacePath>,
|
||||
Extension(actor): Extension<RequestActor>,
|
||||
) -> ApiResult<Json<Vec<RepositorySshHostTrust>>> {
|
||||
require_manage_repository_secrets(&api, &path.workspace_id, &actor).await?;
|
||||
let projection = active_repository_access_projection(&api, &path.workspace_id)?;
|
||||
Ok(Json(
|
||||
api.repository_secrets
|
||||
.list_host_trusts(&path.workspace_id, &projection)?,
|
||||
))
|
||||
}
|
||||
|
||||
async fn scoped_get_repository_ssh_host_trust(
|
||||
State(api): State<WorkspaceApi>,
|
||||
AxumPath(path): AxumPath<ScopedRepositoryHostTrustPath>,
|
||||
Extension(actor): Extension<RequestActor>,
|
||||
) -> ApiResult<Json<RepositorySshHostTrust>> {
|
||||
require_manage_repository_secrets(&api, &path.workspace_id, &actor).await?;
|
||||
let projection = active_repository_access_projection(&api, &path.workspace_id)?;
|
||||
let host_trust = api
|
||||
.repository_secrets
|
||||
.list_host_trusts(&path.workspace_id, &projection)?
|
||||
.into_iter()
|
||||
.find(|host_trust| host_trust.host_trust_id == path.host_trust_id)
|
||||
.ok_or_else(|| Error::InvalidRecordId(path.host_trust_id.clone()))?;
|
||||
Ok(Json(host_trust))
|
||||
}
|
||||
|
||||
async fn scoped_put_repository_ssh_host_trust(
|
||||
State(api): State<WorkspaceApi>,
|
||||
AxumPath(path): AxumPath<ScopedWorkspacePath>,
|
||||
Extension(actor): Extension<RequestActor>,
|
||||
Json(request): Json<PutRepositorySshHostTrustRequest>,
|
||||
) -> ApiResult<(StatusCode, Json<RepositorySshHostTrust>)> {
|
||||
require_manage_repository_secrets(&api, &path.workspace_id, &actor).await?;
|
||||
let status = if request.expected_revision.is_some() {
|
||||
StatusCode::OK
|
||||
} else {
|
||||
StatusCode::CREATED
|
||||
};
|
||||
let host_trust =
|
||||
api.repository_secrets
|
||||
.put_host_trust(&path.workspace_id, request, &actor.account_id)?;
|
||||
Ok((status, Json(host_trust)))
|
||||
}
|
||||
|
||||
async fn scoped_delete_repository_ssh_host_trust(
|
||||
State(api): State<WorkspaceApi>,
|
||||
AxumPath(path): AxumPath<ScopedRepositoryHostTrustPath>,
|
||||
Extension(actor): Extension<RequestActor>,
|
||||
Json(request): Json<DeleteRepositorySshHostTrustRequest>,
|
||||
) -> ApiResult<StatusCode> {
|
||||
require_manage_repository_secrets(&api, &path.workspace_id, &actor).await?;
|
||||
let projection = active_repository_access_projection(&api, &path.workspace_id)?;
|
||||
api.repository_secrets.delete_host_trust(
|
||||
&path.workspace_id,
|
||||
&path.host_trust_id,
|
||||
request,
|
||||
&actor.account_id,
|
||||
&projection,
|
||||
)?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
async fn scoped_get_workspace_config_tree(
|
||||
State(api): State<WorkspaceApi>,
|
||||
AxumPath(path): AxumPath<ScopedWorkspacePath>,
|
||||
@@ -3333,6 +3577,12 @@ async fn scoped_commit_workspace_config_tree(
|
||||
api.config_schema_registry.compose()?,
|
||||
)?;
|
||||
crate::prompt_settings::validate_evaluated_prompt_catalog(&candidate.evaluation)?;
|
||||
project_repository_access_candidate(
|
||||
&*api.store,
|
||||
&api.repository_secrets,
|
||||
&path.workspace_id,
|
||||
&candidate,
|
||||
)?;
|
||||
let state = api
|
||||
.config_store
|
||||
.commit_evaluated_workspace_config(&path.workspace_id, &candidate)?;
|
||||
@@ -14070,7 +14320,9 @@ impl ApiError {
|
||||
impl IntoResponse for ApiError {
|
||||
fn into_response(self) -> Response {
|
||||
let status = match &self.error {
|
||||
Error::BrowserReopenConfirmationRequired => StatusCode::FORBIDDEN,
|
||||
Error::BrowserReopenConfirmationRequired | Error::WorkspacePermissionDenied(_) => {
|
||||
StatusCode::FORBIDDEN
|
||||
}
|
||||
Error::TicketAssignmentConflict(_)
|
||||
| Error::WorkdirAttachmentConflict(_)
|
||||
| Error::WorkspaceConfigConflict(_) => StatusCode::CONFLICT,
|
||||
@@ -19157,6 +19409,54 @@ mod tests {
|
||||
assert_eq!(detail.provenance.id, "workspace:triage-errors");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn repository_secret_management_is_owner_only() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let api = test_api(temp.path()).await;
|
||||
let timestamp = Utc::now().to_rfc3339();
|
||||
api.store
|
||||
.upsert_account(&crate::store::AccountRecord {
|
||||
account_id: "owner-account".to_string(),
|
||||
kind: "user".to_string(),
|
||||
handle: "owner".to_string(),
|
||||
display_name: "Owner".to_string(),
|
||||
created_at: timestamp.clone(),
|
||||
updated_at: timestamp,
|
||||
})
|
||||
.unwrap();
|
||||
let mut workspace = api
|
||||
.store
|
||||
.get_workspace(TEST_WORKSPACE_ID)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
workspace.owner_account_id = Some("owner-account".to_string());
|
||||
api.store.upsert_workspace(&workspace).await.unwrap();
|
||||
|
||||
let owner = RequestActor {
|
||||
user_id: "owner-user".to_string(),
|
||||
account_id: "owner-account".to_string(),
|
||||
handle: "owner".to_string(),
|
||||
display_name: "Owner".to_string(),
|
||||
auth_method: ActorAuthMethod::BrowserSession,
|
||||
};
|
||||
require_manage_repository_secrets(&api, TEST_WORKSPACE_ID, &owner)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let non_owner = RequestActor {
|
||||
user_id: "other-user".to_string(),
|
||||
account_id: "other-account".to_string(),
|
||||
handle: "other".to_string(),
|
||||
display_name: "Other".to_string(),
|
||||
auth_method: ActorAuthMethod::ApiToken,
|
||||
};
|
||||
let error = require_manage_repository_secrets(&api, TEST_WORKSPACE_ID, &non_owner)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(error.into_response().status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
async fn test_api_with_recording_backend(
|
||||
workspace_root: impl Into<PathBuf>,
|
||||
) -> (WorkspaceApi, Arc<DeterministicExecutionBackend>) {
|
||||
|
||||
@@ -252,6 +252,11 @@ const MIGRATIONS: &[Migration] = &[
|
||||
name: "create Workdir create operations",
|
||||
apply: create_workdir_create_operations,
|
||||
},
|
||||
Migration {
|
||||
version: 46,
|
||||
name: "create Workspace Repository SSH secret authority",
|
||||
apply: create_repository_ssh_secret_authority,
|
||||
},
|
||||
];
|
||||
|
||||
struct Migration {
|
||||
@@ -6703,6 +6708,110 @@ fn create_workdir_create_operations(conn: &Connection) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_repository_ssh_secret_authority(conn: &Connection) -> Result<()> {
|
||||
conn.execute_batch(
|
||||
r#"
|
||||
CREATE TABLE repository_ssh_credentials (
|
||||
workspace_id TEXT NOT NULL,
|
||||
credential_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
public_key_algorithm TEXT NOT NULL,
|
||||
public_key_fingerprint TEXT NOT NULL,
|
||||
current_revision INTEGER NOT NULL CHECK (current_revision >= 1),
|
||||
status TEXT NOT NULL CHECK (status IN ('active', 'revoked')),
|
||||
created_at TEXT NOT NULL,
|
||||
rotated_at TEXT,
|
||||
PRIMARY KEY (workspace_id, credential_id),
|
||||
FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE repository_ssh_credential_revisions (
|
||||
workspace_id TEXT NOT NULL,
|
||||
credential_id TEXT NOT NULL,
|
||||
revision INTEGER NOT NULL CHECK (revision >= 1),
|
||||
public_key_algorithm TEXT NOT NULL,
|
||||
public_key_fingerprint TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
PRIMARY KEY (workspace_id, credential_id, revision),
|
||||
FOREIGN KEY (workspace_id, credential_id)
|
||||
REFERENCES repository_ssh_credentials(workspace_id, credential_id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE server_secret_versions (
|
||||
workspace_id TEXT NOT NULL,
|
||||
secret_id TEXT NOT NULL,
|
||||
revision INTEGER NOT NULL CHECK (revision >= 1),
|
||||
purpose TEXT NOT NULL CHECK (purpose IN ('private_key', 'passphrase')),
|
||||
encryption_algorithm TEXT NOT NULL CHECK (encryption_algorithm = 'aes-256-gcm-v1'),
|
||||
nonce BLOB NOT NULL CHECK (length(nonce) = 12),
|
||||
ciphertext BLOB NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
PRIMARY KEY (workspace_id, secret_id, revision, purpose),
|
||||
FOREIGN KEY (workspace_id, secret_id, revision)
|
||||
REFERENCES repository_ssh_credential_revisions(workspace_id, credential_id, revision)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE repository_ssh_host_trusts (
|
||||
workspace_id TEXT NOT NULL,
|
||||
host_trust_id TEXT NOT NULL,
|
||||
hostname TEXT NOT NULL,
|
||||
port INTEGER NOT NULL CHECK (port >= 1 AND port <= 65535),
|
||||
key_algorithm TEXT NOT NULL,
|
||||
host_key TEXT NOT NULL,
|
||||
fingerprint TEXT NOT NULL,
|
||||
current_revision INTEGER NOT NULL CHECK (current_revision >= 1),
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
PRIMARY KEY (workspace_id, host_trust_id),
|
||||
FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE repository_ssh_host_trust_revisions (
|
||||
workspace_id TEXT NOT NULL,
|
||||
host_trust_id TEXT NOT NULL,
|
||||
revision INTEGER NOT NULL CHECK (revision >= 1),
|
||||
hostname TEXT NOT NULL,
|
||||
port INTEGER NOT NULL CHECK (port >= 1 AND port <= 65535),
|
||||
key_algorithm TEXT NOT NULL,
|
||||
host_key TEXT NOT NULL,
|
||||
fingerprint TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
PRIMARY KEY (workspace_id, host_trust_id, revision),
|
||||
FOREIGN KEY (workspace_id, host_trust_id)
|
||||
REFERENCES repository_ssh_host_trusts(workspace_id, host_trust_id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE repository_secret_operations (
|
||||
workspace_id TEXT NOT NULL,
|
||||
operation_id TEXT NOT NULL,
|
||||
request_fingerprint TEXT NOT NULL,
|
||||
resource_kind TEXT NOT NULL CHECK (resource_kind IN ('credential', 'host_trust')),
|
||||
resource_id TEXT NOT NULL,
|
||||
result_revision INTEGER NOT NULL CHECK (result_revision >= 1),
|
||||
created_at TEXT NOT NULL,
|
||||
PRIMARY KEY (workspace_id, operation_id),
|
||||
FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE repository_secret_audit_events (
|
||||
workspace_id TEXT NOT NULL,
|
||||
event_id TEXT NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
resource_id TEXT NOT NULL,
|
||||
revision INTEGER NOT NULL CHECK (revision >= 1),
|
||||
actor_account_id TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
PRIMARY KEY (workspace_id, event_id),
|
||||
FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX idx_repository_ssh_credentials_workspace_status
|
||||
ON repository_ssh_credentials(workspace_id, status, credential_id);
|
||||
CREATE INDEX idx_repository_ssh_host_trusts_workspace_host
|
||||
ON repository_ssh_host_trusts(workspace_id, hostname, port);
|
||||
CREATE INDEX idx_repository_secret_audit_workspace_created
|
||||
ON repository_secret_audit_events(workspace_id, created_at, event_id);
|
||||
"#,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_workspace_catalog_operations(conn: &Connection) -> Result<()> {
|
||||
conn.execute_batch(
|
||||
r#"
|
||||
@@ -9588,7 +9697,7 @@ mod tests {
|
||||
|
||||
apply_migrations(&conn).unwrap();
|
||||
|
||||
assert_eq!(current_schema_version(&conn).unwrap(), 45);
|
||||
assert_eq!(current_schema_version(&conn).unwrap(), 46);
|
||||
let remote = conn
|
||||
.query_row(
|
||||
"SELECT source_kind, source_uri, source_revision, source_fingerprint, observed_status \
|
||||
@@ -9666,7 +9775,7 @@ mod tests {
|
||||
let before = std::fs::read(&path).unwrap();
|
||||
let plan = SqliteWorkspaceStore::migration_plan(&path).unwrap();
|
||||
assert_eq!(plan.current_schema_version, 36);
|
||||
assert_eq!(plan.target_schema_version, 45);
|
||||
assert_eq!(plan.target_schema_version, 46);
|
||||
assert!(plan.migration_required);
|
||||
assert_eq!(plan.worker_count, 1);
|
||||
assert_eq!(plan.mappings[0].legacy_worker_id, 7);
|
||||
@@ -9680,7 +9789,7 @@ mod tests {
|
||||
store
|
||||
.with_conn(|conn| {
|
||||
assert!(table_exists(conn, "worker_diagnostics_archives")?);
|
||||
assert_eq!(current_schema_version(conn)?, 45);
|
||||
assert_eq!(current_schema_version(conn)?, 46);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
@@ -9816,7 +9925,7 @@ mod tests {
|
||||
),
|
||||
]
|
||||
);
|
||||
assert_eq!(current_schema_version(&conn).unwrap(), 45);
|
||||
assert_eq!(current_schema_version(&conn).unwrap(), 46);
|
||||
let foreign_key_error: Option<String> = conn
|
||||
.query_row("PRAGMA foreign_key_check", [], |row| row.get(0))
|
||||
.optional()
|
||||
@@ -9945,7 +10054,7 @@ INSERT INTO worker_orphan_diagnostics (
|
||||
|
||||
apply_migrations(&conn).unwrap();
|
||||
|
||||
assert_eq!(current_schema_version(&conn).unwrap(), 45);
|
||||
assert_eq!(current_schema_version(&conn).unwrap(), 46);
|
||||
assert!(!table_exists(&conn, "worker_control_delegation_operations").unwrap());
|
||||
let controller_worker_id: String = conn
|
||||
.query_row(
|
||||
@@ -10063,7 +10172,7 @@ INSERT INTO worker_orphan_diagnostics (
|
||||
|
||||
apply_migrations(&conn).unwrap();
|
||||
|
||||
assert_eq!(current_schema_version(&conn).unwrap(), 45);
|
||||
assert_eq!(current_schema_version(&conn).unwrap(), 46);
|
||||
assert!(table_exists(&conn, "worker_workdir_attachment_reservations").unwrap());
|
||||
}
|
||||
|
||||
@@ -10081,7 +10190,7 @@ INSERT INTO worker_orphan_diagnostics (
|
||||
|
||||
apply_migrations(&conn).unwrap();
|
||||
|
||||
assert_eq!(current_schema_version(&conn).unwrap(), 45);
|
||||
assert_eq!(current_schema_version(&conn).unwrap(), 46);
|
||||
let settings = conn
|
||||
.query_row(
|
||||
"SELECT settings_revision, language FROM workspace_memory_settings \
|
||||
@@ -10122,7 +10231,7 @@ CREATE TABLE flow_events (event_id TEXT PRIMARY KEY);
|
||||
|
||||
apply_migrations(&conn).unwrap();
|
||||
|
||||
assert_eq!(current_schema_version(&conn).unwrap(), 45);
|
||||
assert_eq!(current_schema_version(&conn).unwrap(), 46);
|
||||
assert!(table_exists(&conn, "flow_sources").unwrap());
|
||||
assert!(table_exists(&conn, "flow_source_revisions").unwrap());
|
||||
assert!(!table_exists(&conn, "flow_instances").unwrap());
|
||||
@@ -10189,7 +10298,7 @@ INSERT INTO worker_workdir_attachment_reservations (
|
||||
|
||||
apply_migrations(&conn).unwrap();
|
||||
|
||||
assert_eq!(current_schema_version(&conn).unwrap(), 45);
|
||||
assert_eq!(current_schema_version(&conn).unwrap(), 46);
|
||||
let repositories_sql: String = conn
|
||||
.query_row(
|
||||
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'repositories'",
|
||||
@@ -10372,7 +10481,7 @@ INSERT INTO workdir_registry (
|
||||
let db = dir.path().join("control-plane.sqlite");
|
||||
let store = SqliteWorkspaceStore::open(&db).unwrap();
|
||||
|
||||
assert_eq!(store.schema_version().await.unwrap(), 45);
|
||||
assert_eq!(store.schema_version().await.unwrap(), 46);
|
||||
assert!(
|
||||
!store
|
||||
.with_conn(|conn| table_exists(conn, "worker_workspace_credentials"))
|
||||
@@ -10389,7 +10498,7 @@ INSERT INTO workdir_registry (
|
||||
store.upsert_workspace(&record).await.unwrap();
|
||||
|
||||
let reopened = SqliteWorkspaceStore::open(&db).unwrap();
|
||||
assert_eq!(reopened.schema_version().await.unwrap(), 45);
|
||||
assert_eq!(reopened.schema_version().await.unwrap(), 46);
|
||||
assert_eq!(
|
||||
reopened.get_workspace("local-dev").await.unwrap(),
|
||||
Some(record)
|
||||
@@ -11143,7 +11252,7 @@ INSERT INTO worker_registry (
|
||||
let migrated = SqliteWorkspaceStore::open(&db_path).unwrap();
|
||||
migrated
|
||||
.with_conn(|conn| {
|
||||
assert_eq!(current_schema_version(conn)?, 45);
|
||||
assert_eq!(current_schema_version(conn)?, 46);
|
||||
assert_eq!(
|
||||
conn.query_row("PRAGMA foreign_keys", [], |row| row.get::<_, i64>(0))?,
|
||||
1,
|
||||
@@ -11492,14 +11601,21 @@ INSERT INTO worker_registry (
|
||||
configure_sqlite(&conn).unwrap();
|
||||
apply_migrations(&conn).unwrap();
|
||||
conn.execute_batch(
|
||||
"DROP TABLE workdir_create_operations;
|
||||
DELETE FROM __yoi_schema_migrations WHERE version = 45;",
|
||||
"DROP TABLE repository_secret_audit_events;
|
||||
DROP TABLE repository_secret_operations;
|
||||
DROP TABLE server_secret_versions;
|
||||
DROP TABLE repository_ssh_credential_revisions;
|
||||
DROP TABLE repository_ssh_credentials;
|
||||
DROP TABLE repository_ssh_host_trust_revisions;
|
||||
DROP TABLE repository_ssh_host_trusts;
|
||||
DROP TABLE workdir_create_operations;
|
||||
DELETE FROM __yoi_schema_migrations WHERE version IN (45, 46);",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(current_schema_version(&conn).unwrap(), 44);
|
||||
|
||||
apply_migrations(&conn).unwrap();
|
||||
assert_eq!(current_schema_version(&conn).unwrap(), 45);
|
||||
assert_eq!(current_schema_version(&conn).unwrap(), 46);
|
||||
assert!(table_exists(&conn, "workdir_create_operations").unwrap());
|
||||
let columns = table_columns(&conn, "workdir_create_operations").unwrap();
|
||||
for required in [
|
||||
@@ -11518,19 +11634,57 @@ INSERT INTO worker_registry (
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema_v46_adds_repository_ssh_secret_authority_to_v45_database() {
|
||||
let conn = Connection::open_in_memory().unwrap();
|
||||
configure_sqlite(&conn).unwrap();
|
||||
apply_migrations(&conn).unwrap();
|
||||
conn.execute_batch(
|
||||
"DROP TABLE repository_secret_audit_events;
|
||||
DROP TABLE repository_secret_operations;
|
||||
DROP TABLE server_secret_versions;
|
||||
DROP TABLE repository_ssh_credential_revisions;
|
||||
DROP TABLE repository_ssh_credentials;
|
||||
DROP TABLE repository_ssh_host_trust_revisions;
|
||||
DROP TABLE repository_ssh_host_trusts;
|
||||
DELETE FROM __yoi_schema_migrations WHERE version = 46;",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(current_schema_version(&conn).unwrap(), 45);
|
||||
|
||||
apply_migrations(&conn).unwrap();
|
||||
assert_eq!(current_schema_version(&conn).unwrap(), 46);
|
||||
for table in [
|
||||
"repository_ssh_credentials",
|
||||
"repository_ssh_credential_revisions",
|
||||
"server_secret_versions",
|
||||
"repository_ssh_host_trusts",
|
||||
"repository_ssh_host_trust_revisions",
|
||||
"repository_secret_operations",
|
||||
"repository_secret_audit_events",
|
||||
] {
|
||||
assert!(table_exists(&conn, table).unwrap(), "missing table {table}");
|
||||
}
|
||||
let foreign_key_error: Option<String> = conn
|
||||
.query_row("PRAGMA foreign_key_check", [], |row| row.get(0))
|
||||
.optional()
|
||||
.unwrap();
|
||||
assert!(foreign_key_error.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_refuses_a_database_from_a_newer_schema_generation() {
|
||||
let conn = Connection::open_in_memory().unwrap();
|
||||
configure_sqlite(&conn).unwrap();
|
||||
apply_migrations(&conn).unwrap();
|
||||
conn.execute(
|
||||
"INSERT INTO __yoi_schema_migrations (version, name) VALUES (46, 'future')",
|
||||
"INSERT INTO __yoi_schema_migrations (version, name) VALUES (47, 'future')",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let error = apply_migrations(&conn).unwrap_err().to_string();
|
||||
assert!(error.contains("schema version 46 is newer"), "{error}");
|
||||
assert!(error.contains("schema version 47 is newer"), "{error}");
|
||||
assert!(error.contains("refusing to serve"), "{error}");
|
||||
}
|
||||
|
||||
@@ -11751,7 +11905,7 @@ VALUES ('workspace-b', 'ticket-b', 'related', 'ticket-a', NULL, 'tester', '2026-
|
||||
|
||||
apply_migrations(&mut conn).unwrap();
|
||||
|
||||
assert_eq!(current_schema_version(&conn).unwrap(), 45);
|
||||
assert_eq!(current_schema_version(&conn).unwrap(), 46);
|
||||
let workspace_id: Option<String> = conn
|
||||
.query_row(
|
||||
"SELECT workspace_id FROM trusted_runtime_records WHERE runtime_id = 'runtime-a'",
|
||||
@@ -12374,7 +12528,7 @@ WHERE workspace_id = 'workspace-a'
|
||||
.unwrap();
|
||||
|
||||
let store = SqliteWorkspaceStore::from_connection(conn).unwrap();
|
||||
assert_eq!(store.schema_version().await.unwrap(), 45);
|
||||
assert_eq!(store.schema_version().await.unwrap(), 46);
|
||||
|
||||
store
|
||||
.with_conn(|conn| {
|
||||
@@ -12563,7 +12717,7 @@ CREATE TABLE ticket_assignment_operations (
|
||||
#[tokio::test]
|
||||
async fn repository_records_round_trip() {
|
||||
let store = SqliteWorkspaceStore::in_memory().unwrap();
|
||||
assert_eq!(store.schema_version().await.unwrap(), 45);
|
||||
assert_eq!(store.schema_version().await.unwrap(), 46);
|
||||
let workspace = WorkspaceRecord {
|
||||
workspace_id: "local-dev".to_string(),
|
||||
owner_account_id: None,
|
||||
@@ -12641,7 +12795,7 @@ CREATE TABLE ticket_assignment_operations (
|
||||
#[tokio::test]
|
||||
async fn memory_authority_records_round_trip_and_close_staging() {
|
||||
let store = SqliteWorkspaceStore::in_memory().unwrap();
|
||||
assert_eq!(store.schema_version().await.unwrap(), 45);
|
||||
assert_eq!(store.schema_version().await.unwrap(), 46);
|
||||
let workspace = WorkspaceRecord {
|
||||
workspace_id: "local-dev".to_string(),
|
||||
owner_account_id: None,
|
||||
@@ -13048,7 +13202,7 @@ CREATE TABLE ticket_assignment_operations (
|
||||
#[tokio::test]
|
||||
async fn account_and_login_records_round_trip() {
|
||||
let store = SqliteWorkspaceStore::in_memory().unwrap();
|
||||
assert_eq!(store.schema_version().await.unwrap(), 45);
|
||||
assert_eq!(store.schema_version().await.unwrap(), 46);
|
||||
let now = "2026-07-22T00:00:00Z".to_string();
|
||||
let account = AccountRecord {
|
||||
account_id: "acct-user-alice".to_string(),
|
||||
|
||||
@@ -38,14 +38,36 @@ Deno.test("settings section navigation stays under the settings route", () => {
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("settings shell advertises no fake browser admin model", () => {
|
||||
Deno.test("settings shell advertises scoped account authority", () => {
|
||||
assert(
|
||||
SETTINGS_PERMISSION_NOTICE.includes("no browser user, role, permission"),
|
||||
"notice should explicitly deny a browser permission model",
|
||||
SETTINGS_PERMISSION_NOTICE.includes("authenticated account authority"),
|
||||
"notice should identify authenticated account authority",
|
||||
);
|
||||
assert(
|
||||
SETTINGS_PERMISSION_NOTICE.includes("does not create an admin role"),
|
||||
"notice should not imply an admin role exists",
|
||||
SETTINGS_PERMISSION_NOTICE.includes("current Workspace owner"),
|
||||
"notice should state the Repository secret permission boundary",
|
||||
);
|
||||
assert(
|
||||
SETTINGS_PERMISSION_NOTICE.includes("does not expose secret material"),
|
||||
"notice should not imply that stored secret material is readable",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("Repository access settings are editable and canonically routed", () => {
|
||||
const section = SETTINGS_SECTIONS.find((entry) =>
|
||||
entry.id === "repository-access"
|
||||
);
|
||||
assert(
|
||||
section?.status === "editable",
|
||||
"Repository Access should be editable",
|
||||
);
|
||||
assert(
|
||||
settingsSectionHref("repository-access") === "/settings/repository-access",
|
||||
"Repository Access should have a dedicated settings route",
|
||||
);
|
||||
assert(
|
||||
section?.bullets.join("\n").includes("write-only"),
|
||||
"Repository Access copy should preserve write-only secret semantics",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ export type SettingsSectionId =
|
||||
| "runtime-connections"
|
||||
| "runtime-inventory"
|
||||
| "configuration-sources"
|
||||
| "repository-access"
|
||||
| "profile-sources"
|
||||
| "backend-config"
|
||||
| "workspace-identity";
|
||||
@@ -72,7 +73,7 @@ export type RemoteRuntimeTestResponse = {
|
||||
export const SETTINGS_ROUTE = "/settings";
|
||||
|
||||
export const SETTINGS_PERMISSION_NOTICE =
|
||||
"Yoi currently has no browser user, role, permission, or multi-user authorization model. This local settings surface uses typed Backend APIs only; it does not create an admin role or grant broad mutation authority.";
|
||||
"Workspace settings use authenticated account authority and Workspace-scoped typed Backend APIs. Repository secret management requires the current Workspace owner; this surface does not expose secret material or grant Runtime execution authority.";
|
||||
|
||||
export const SETTINGS_SECTIONS: readonly SettingsSection[] = [
|
||||
{
|
||||
@@ -111,6 +112,18 @@ export const SETTINGS_SECTIONS: readonly SettingsSection[] = [
|
||||
"Profile launch data is projected from this active revision; remaining Skill, Prompt, and Plugin consumers migrate in their follow-up cutovers.",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "repository-access",
|
||||
label: "Repository Access",
|
||||
status: "editable",
|
||||
summary:
|
||||
"Manage Workspace-scoped SSH credentials and pinned host keys without exposing stored secret material.",
|
||||
bullets: [
|
||||
"Private keys and passphrases are write-only; list and detail responses contain public metadata only.",
|
||||
"Host trust requires an explicitly pinned key and never uses accept-new or TOFU.",
|
||||
"Repository bindings are committed through the shared Workspace configuration editor and validated against these records.",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "profile-sources",
|
||||
label: "Profile Sources",
|
||||
@@ -175,6 +188,8 @@ export function settingsSectionHref(id: SettingsSectionId): string {
|
||||
return `${SETTINGS_ROUTE}/runtimes`;
|
||||
case "configuration-sources":
|
||||
return `${SETTINGS_ROUTE}/configuration`;
|
||||
case "repository-access":
|
||||
return `${SETTINGS_ROUTE}/repository-access`;
|
||||
case "profile-sources":
|
||||
return `${SETTINGS_ROUTE}/profiles`;
|
||||
case "workspace-identity":
|
||||
|
||||
@@ -31,6 +31,10 @@
|
||||
<dt>Source</dt>
|
||||
<dd>{data.repository.item.source.kind} · {data.repository.item.source.uri}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Repository access</dt>
|
||||
<dd><a href={`/w/${encodeURIComponent(data.workspace.workspace_id)}/settings/repository-access`}>Manage SSH credentials and pinned host keys</a></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Source revision</dt>
|
||||
<dd>{data.repository.item.source_revision} · {data.repository.item.source_fingerprint}</dd>
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte';
|
||||
import type { PageProps } from './$types';
|
||||
import type { RepositorySshCredential, RepositorySshHostTrust } from './+page';
|
||||
|
||||
let { data }: PageProps = $props();
|
||||
let credentials = $state<RepositorySshCredential[]>(untrack(() => data.credentials));
|
||||
let hostTrusts = $state<RepositorySshHostTrust[]>(untrack(() => data.hostTrusts));
|
||||
let message = $state<string | null>(null);
|
||||
let pending = $state(false);
|
||||
|
||||
let credentialId = $state('');
|
||||
let credentialName = $state('');
|
||||
let privateKey = $state('');
|
||||
let passphrase = $state('');
|
||||
let rotateCredentialId = $state<string | null>(null);
|
||||
let rotatePrivateKey = $state('');
|
||||
let rotatePassphrase = $state('');
|
||||
|
||||
let hostTrustId = $state('');
|
||||
let hostname = $state('');
|
||||
let port = $state(22);
|
||||
let hostKey = $state('');
|
||||
let hostExpectedRevision = $state<number | null>(null);
|
||||
|
||||
const base = $derived(`/api/w/${encodeURIComponent(data.workspaceId)}/settings/repository-access`);
|
||||
|
||||
function operationId(prefix: string): string {
|
||||
return `${prefix}-${crypto.randomUUID()}`;
|
||||
}
|
||||
|
||||
async function request<T>(path: string, method: string, body: unknown): Promise<T> {
|
||||
const response = await fetch(`${base}${path}`, {
|
||||
method,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
if (!response.ok) {
|
||||
let detail = `request failed (${response.status})`;
|
||||
try {
|
||||
const payload = (await response.json()) as { error?: string; message?: string };
|
||||
detail = payload.message ?? payload.error ?? detail;
|
||||
} catch {
|
||||
// Do not surface submitted secret values from response bodies.
|
||||
}
|
||||
throw new Error(detail);
|
||||
}
|
||||
if (response.status === 204) return undefined as T;
|
||||
return (await response.json()) as T;
|
||||
}
|
||||
|
||||
async function createCredential() {
|
||||
pending = true;
|
||||
message = null;
|
||||
try {
|
||||
const created = await request<RepositorySshCredential>('/credentials', 'POST', {
|
||||
operation_id: operationId('credential-create'),
|
||||
credential_id: credentialId,
|
||||
name: credentialName,
|
||||
private_key: privateKey,
|
||||
passphrase: passphrase || null
|
||||
});
|
||||
credentials = [...credentials, created].sort((a, b) => a.credential_id.localeCompare(b.credential_id));
|
||||
credentialId = '';
|
||||
credentialName = '';
|
||||
message = `Credential ${created.credential_id} created. Pasted secret fields were cleared.`;
|
||||
} catch (error) {
|
||||
message = error instanceof Error ? error.message : 'Credential creation failed';
|
||||
} finally {
|
||||
privateKey = '';
|
||||
passphrase = '';
|
||||
pending = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function rotateCredential(credential: RepositorySshCredential) {
|
||||
pending = true;
|
||||
message = null;
|
||||
try {
|
||||
const rotated = await request<RepositorySshCredential>(
|
||||
`/credentials/${encodeURIComponent(credential.credential_id)}/rotate`,
|
||||
'POST',
|
||||
{
|
||||
operation_id: operationId('credential-rotate'),
|
||||
expected_revision: credential.current_revision,
|
||||
private_key: rotatePrivateKey,
|
||||
passphrase: rotatePassphrase || null
|
||||
}
|
||||
);
|
||||
credentials = credentials.map((entry) => entry.credential_id === rotated.credential_id ? rotated : entry);
|
||||
rotateCredentialId = null;
|
||||
message = `Credential ${rotated.credential_id} rotated to revision ${rotated.current_revision}. Pasted secret fields were cleared.`;
|
||||
} catch (error) {
|
||||
message = error instanceof Error ? error.message : 'Credential rotation failed';
|
||||
} finally {
|
||||
rotatePrivateKey = '';
|
||||
rotatePassphrase = '';
|
||||
pending = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteCredential(credential: RepositorySshCredential) {
|
||||
if (!confirm(`Delete credential ${credential.credential_id}?`)) return;
|
||||
pending = true;
|
||||
message = null;
|
||||
try {
|
||||
await request(`/credentials/${encodeURIComponent(credential.credential_id)}`, 'DELETE', {
|
||||
operation_id: operationId('credential-delete'),
|
||||
expected_revision: credential.current_revision
|
||||
});
|
||||
credentials = credentials.filter((entry) => entry.credential_id !== credential.credential_id);
|
||||
message = `Credential ${credential.credential_id} deleted.`;
|
||||
} catch (error) {
|
||||
message = error instanceof Error ? error.message : 'Credential deletion failed';
|
||||
} finally {
|
||||
pending = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function createHostTrust() {
|
||||
pending = true;
|
||||
message = null;
|
||||
try {
|
||||
const created = await request<RepositorySshHostTrust>('/host-trusts', 'POST', {
|
||||
operation_id: operationId('host-trust-create'),
|
||||
host_trust_id: hostTrustId,
|
||||
hostname,
|
||||
port,
|
||||
host_key: hostKey,
|
||||
expected_revision: hostExpectedRevision
|
||||
});
|
||||
hostTrusts = hostExpectedRevision === null
|
||||
? [...hostTrusts, created].sort((a, b) => a.host_trust_id.localeCompare(b.host_trust_id))
|
||||
: hostTrusts.map((entry) => entry.host_trust_id === created.host_trust_id ? created : entry);
|
||||
hostTrustId = '';
|
||||
hostname = '';
|
||||
port = 22;
|
||||
hostKey = '';
|
||||
hostExpectedRevision = null;
|
||||
message = `Host trust ${created.host_trust_id} saved at revision ${created.current_revision}.`;
|
||||
} catch (error) {
|
||||
message = error instanceof Error ? error.message : 'Host trust creation failed';
|
||||
} finally {
|
||||
pending = false;
|
||||
}
|
||||
}
|
||||
|
||||
function editHostTrust(hostTrust: RepositorySshHostTrust) {
|
||||
hostTrustId = hostTrust.host_trust_id;
|
||||
hostname = hostTrust.hostname;
|
||||
port = hostTrust.port;
|
||||
hostKey = hostTrust.host_key;
|
||||
hostExpectedRevision = hostTrust.current_revision;
|
||||
}
|
||||
|
||||
async function deleteHostTrust(hostTrust: RepositorySshHostTrust) {
|
||||
if (!confirm(`Delete host trust ${hostTrust.host_trust_id}?`)) return;
|
||||
pending = true;
|
||||
message = null;
|
||||
try {
|
||||
await request(`/host-trusts/${encodeURIComponent(hostTrust.host_trust_id)}`, 'DELETE', {
|
||||
operation_id: operationId('host-trust-delete'),
|
||||
expected_revision: hostTrust.current_revision
|
||||
});
|
||||
hostTrusts = hostTrusts.filter((entry) => entry.host_trust_id !== hostTrust.host_trust_id);
|
||||
message = `Host trust ${hostTrust.host_trust_id} deleted.`;
|
||||
} catch (error) {
|
||||
message = error instanceof Error ? error.message : 'Host trust deletion failed';
|
||||
} finally {
|
||||
pending = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head><title>Repository Access · Yoi Workspace</title></svelte:head>
|
||||
|
||||
<section class="card settings-section">
|
||||
<header class="settings-section-header">
|
||||
<div><p class="eyebrow">owner only</p><h2>Repository Access</h2></div>
|
||||
<span class="badge success">encrypted</span>
|
||||
</header>
|
||||
<p>Manage Workspace-scoped SSH credentials and pinned host keys. Private keys and passphrases are write-only and never returned by this page.</p>
|
||||
{#if message}<p class="status-message">{message}</p>{/if}
|
||||
|
||||
<div class="settings-runtime-list">
|
||||
<h3>SSH credentials</h3>
|
||||
{#if credentials.length === 0}<p>No credentials configured.</p>{/if}
|
||||
{#each credentials as credential (credential.credential_id)}
|
||||
<div class="card">
|
||||
<strong>{credential.name}</strong> <code>{credential.credential_id}</code>
|
||||
<p>{credential.public_key_algorithm} · {credential.public_key_fingerprint} · revision {credential.current_revision}</p>
|
||||
<p>References: {credential.referenced_repositories.join(', ') || 'none'}</p>
|
||||
<div class="settings-action-row">
|
||||
<button type="button" onclick={() => (rotateCredentialId = rotateCredentialId === credential.credential_id ? null : credential.credential_id)}>Rotate</button>
|
||||
<button type="button" class="danger" disabled={pending || credential.referenced_repositories.length > 0} onclick={() => void deleteCredential(credential)}>Delete</button>
|
||||
</div>
|
||||
{#if rotateCredentialId === credential.credential_id}
|
||||
<form class="settings-runtime-form" onsubmit={(event) => { event.preventDefault(); void rotateCredential(credential); }}>
|
||||
<label><span>New private key</span><textarea bind:value={rotatePrivateKey} required rows="8" autocomplete="off"></textarea></label>
|
||||
<label><span>Passphrase (only for an encrypted key)</span><input type="password" bind:value={rotatePassphrase} autocomplete="new-password" /></label>
|
||||
<button type="submit" disabled={pending}>Rotate credential</button>
|
||||
</form>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<form class="settings-runtime-form" onsubmit={(event) => { event.preventDefault(); void createCredential(); }}>
|
||||
<h3>Add SSH credential</h3>
|
||||
<label><span>Credential id</span><input bind:value={credentialId} required pattern="[A-Za-z0-9_.-]+" maxlength="128" /></label>
|
||||
<label><span>Name</span><input bind:value={credentialName} required maxlength="200" /></label>
|
||||
<label><span>OpenSSH private key (ssh-ed25519)</span><textarea bind:value={privateKey} required rows="10" autocomplete="off"></textarea></label>
|
||||
<label><span>Passphrase (only for an encrypted key)</span><input type="password" bind:value={passphrase} autocomplete="new-password" /></label>
|
||||
<button type="submit" disabled={pending}>Add credential</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="settings-runtime-list">
|
||||
<h3>Pinned SSH host keys</h3>
|
||||
{#if hostTrusts.length === 0}<p>No host trust records configured.</p>{/if}
|
||||
{#each hostTrusts as hostTrust (hostTrust.host_trust_id)}
|
||||
<div class="card">
|
||||
<strong>{hostTrust.hostname}:{hostTrust.port}</strong> <code>{hostTrust.host_trust_id}</code>
|
||||
<p>{hostTrust.key_algorithm} · {hostTrust.fingerprint} · revision {hostTrust.current_revision}</p>
|
||||
<p>References: {hostTrust.referenced_repositories.join(', ') || 'none'}</p>
|
||||
<div class="settings-action-row">
|
||||
<button type="button" onclick={() => editHostTrust(hostTrust)}>Rotate key</button>
|
||||
<button type="button" class="danger" disabled={pending || hostTrust.referenced_repositories.length > 0} onclick={() => void deleteHostTrust(hostTrust)}>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<form class="settings-runtime-form" onsubmit={(event) => { event.preventDefault(); void createHostTrust(); }}>
|
||||
<h3>{hostExpectedRevision === null ? 'Add pinned host key' : 'Rotate pinned host key'}</h3>
|
||||
<label><span>Host trust id</span><input bind:value={hostTrustId} disabled={hostExpectedRevision !== null} required pattern="[A-Za-z0-9_.-]+" maxlength="128" /></label>
|
||||
<label><span>Hostname</span><input bind:value={hostname} required /></label>
|
||||
<label><span>Port</span><input type="number" bind:value={port} min="1" max="65535" required /></label>
|
||||
<label><span>OpenSSH public host key (ssh-ed25519)</span><textarea bind:value={hostKey} required rows="4"></textarea></label>
|
||||
<button type="submit" disabled={pending}>{hostExpectedRevision === null ? 'Add host key' : 'Save new revision'}</button>
|
||||
{#if hostExpectedRevision !== null}<button type="button" onclick={() => { hostTrustId = ''; hostname = ''; port = 22; hostKey = ''; hostExpectedRevision = null; }}>Cancel</button>{/if}
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { PageLoad } from "./$types";
|
||||
import { loadJson } from "$lib/workspace/api/http";
|
||||
|
||||
export interface RepositorySshCredential {
|
||||
credential_id: string;
|
||||
workspace_id: string;
|
||||
name: string;
|
||||
public_key_algorithm: string;
|
||||
public_key_fingerprint: string;
|
||||
current_revision: number;
|
||||
status: string;
|
||||
created_at: string;
|
||||
rotated_at: string | null;
|
||||
referenced_repositories: string[];
|
||||
}
|
||||
|
||||
export interface RepositorySshHostTrust {
|
||||
host_trust_id: string;
|
||||
workspace_id: string;
|
||||
hostname: string;
|
||||
port: number;
|
||||
key_algorithm: string;
|
||||
host_key: string;
|
||||
fingerprint: string;
|
||||
current_revision: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
referenced_repositories: string[];
|
||||
}
|
||||
|
||||
export const load: PageLoad = async ({ fetch, params }) => {
|
||||
const base = `/api/w/${
|
||||
encodeURIComponent(params.workspaceId)
|
||||
}/settings/repository-access`;
|
||||
const [credentialResult, hostTrustResult] = await Promise.all([
|
||||
loadJson<RepositorySshCredential[]>(fetch, `${base}/credentials`),
|
||||
loadJson<RepositorySshHostTrust[]>(fetch, `${base}/host-trusts`),
|
||||
]);
|
||||
if (!credentialResult.data || !hostTrustResult.data) {
|
||||
throw new Error(
|
||||
credentialResult.error ?? hostTrustResult.error ??
|
||||
"Repository access settings unavailable",
|
||||
);
|
||||
}
|
||||
return {
|
||||
workspaceId: params.workspaceId,
|
||||
credentials: credentialResult.data,
|
||||
hostTrusts: hostTrustResult.data,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
type TestRegistrar = (name: string, body: () => void | Promise<void>) => void;
|
||||
|
||||
const test =
|
||||
(globalThis as unknown as { Deno: { test: TestRegistrar } }).Deno.test;
|
||||
|
||||
function assert(condition: boolean, message: string): asserts condition {
|
||||
if (!condition) throw new Error(message);
|
||||
}
|
||||
|
||||
const source = await Deno.readTextFile(
|
||||
new URL(
|
||||
"../../src/routes/w/[workspaceId]/settings/repository-access/+page.svelte",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
|
||||
test("Repository credential submissions clear write-only fields in finally blocks", () => {
|
||||
const createStart = source.indexOf("async function createCredential()");
|
||||
const rotateStart = source.indexOf("async function rotateCredential(");
|
||||
const deleteStart = source.indexOf("async function deleteCredential(");
|
||||
assert(
|
||||
createStart >= 0 && rotateStart > createStart && deleteStart > rotateStart,
|
||||
"credential handlers should appear in source order",
|
||||
);
|
||||
|
||||
const createBody = source.slice(createStart, rotateStart);
|
||||
const rotateBody = source.slice(rotateStart, deleteStart);
|
||||
for (const token of ["finally", "privateKey = ''", "passphrase = ''"]) {
|
||||
assert(
|
||||
createBody.includes(token),
|
||||
`create handler should contain ${token}`,
|
||||
);
|
||||
}
|
||||
for (
|
||||
const token of [
|
||||
"finally",
|
||||
"rotatePrivateKey = ''",
|
||||
"rotatePassphrase = ''",
|
||||
]
|
||||
) {
|
||||
assert(
|
||||
rotateBody.includes(token),
|
||||
`rotate handler should contain ${token}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user