In-system image storage (#2112)
* In-system image storage refers to #2057 * adding multipart feature to axum * thanks to @Firstyear for fixing my bufs * fixing coverage test things * clippy-calming * more tests, jpg acropalypse tests, benches * spelling * lockfile updates * linting
348
Cargo.lock
generated
|
@ -42,9 +42,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.0.4"
|
version = "1.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a"
|
checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
@ -92,15 +92,15 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstyle"
|
name = "anstyle"
|
||||||
version = "1.0.2"
|
version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea"
|
checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstyle-parse"
|
name = "anstyle-parse"
|
||||||
version = "0.2.1"
|
version = "0.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333"
|
checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"utf8parse",
|
"utf8parse",
|
||||||
]
|
]
|
||||||
|
@ -189,9 +189,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-compression"
|
name = "async-compression"
|
||||||
version = "0.4.1"
|
version = "0.4.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "62b74f44609f0f91493e3082d3734d98497e094777144380ea4db9f9905dd5b6"
|
checksum = "bb42b2197bf15ccb092b62c74515dbd8b86d0effd934795f6687c93b6e679a2c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"flate2",
|
"flate2",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
@ -270,7 +270,7 @@ dependencies = [
|
||||||
"serde_bytes",
|
"serde_bytes",
|
||||||
"serde_cbor",
|
"serde_cbor",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2 0.10.7",
|
"sha2 0.10.8",
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -300,6 +300,7 @@ dependencies = [
|
||||||
"matchit",
|
"matchit",
|
||||||
"memchr",
|
"memchr",
|
||||||
"mime",
|
"mime",
|
||||||
|
"multer",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"rustversion",
|
"rustversion",
|
||||||
|
@ -464,7 +465,7 @@ dependencies = [
|
||||||
"lazycell",
|
"lazycell",
|
||||||
"log",
|
"log",
|
||||||
"peeking_take_while",
|
"peeking_take_while",
|
||||||
"prettyplease 0.2.12",
|
"prettyplease 0.2.15",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"regex",
|
"regex",
|
||||||
|
@ -574,9 +575,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.13.0"
|
version = "3.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
|
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byte-tools"
|
name = "byte-tools"
|
||||||
|
@ -586,9 +587,9 @@ checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytemuck"
|
name = "bytemuck"
|
||||||
version = "1.13.1"
|
version = "1.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea"
|
checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
|
@ -1029,9 +1030,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "csv"
|
name = "csv"
|
||||||
version = "1.2.2"
|
version = "1.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "626ae34994d3d8d668f4269922248239db4ae42d538b14c398b74a52208e8086"
|
checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"csv-core",
|
"csv-core",
|
||||||
"itoa",
|
"itoa",
|
||||||
|
@ -1041,9 +1042,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "csv-core"
|
name = "csv-core"
|
||||||
version = "0.1.10"
|
version = "0.1.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90"
|
checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
@ -1370,18 +1371,18 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "enum-map"
|
name = "enum-map"
|
||||||
version = "2.6.1"
|
version = "2.6.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9705d8de4776df900a4a0b2384f8b0ab42f775e93b083b42f8ce71bdc32a47e3"
|
checksum = "c188012f8542dee7b3996e44dd89461d64aa471b0a7c71a1ae2f595d259e96e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"enum-map-derive",
|
"enum-map-derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "enum-map-derive"
|
name = "enum-map-derive"
|
||||||
version = "0.13.0"
|
version = "0.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ccb14d927583dd5c2eac0f2cf264fc4762aefe1ae14c47a8a20fc1939d3a5fc0"
|
checksum = "04d0b288e3bb1d861c4403c1774a6f7a798781dfc519b3647df2a3dd4ae95f25"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -1390,18 +1391,18 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "enumflags2"
|
name = "enumflags2"
|
||||||
version = "0.7.7"
|
version = "0.7.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c041f5090df68b32bcd905365fd51769c8b9d553fe87fde0b683534f10c01bd2"
|
checksum = "5998b4f30320c9d93aed72f63af821bfdac50465b75428fce77b48ec482c3939"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"enumflags2_derive",
|
"enumflags2_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "enumflags2_derive"
|
name = "enumflags2_derive"
|
||||||
version = "0.7.7"
|
version = "0.7.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5e9a1f9f7d83e59740248a6e14ecf93929ade55027844dfcea78beafccc15745"
|
checksum = "f95e2801cd355d4a1a3e3953ce6ee5ae9603a5c833455343a8bfe3f44d418246"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -1437,9 +1438,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
version = "0.3.2"
|
version = "0.3.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f"
|
checksum = "add4f07d43996f76ef320709726a556a9d4f965d9410d8d0271132d2f8293480"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"errno-dragonfly",
|
"errno-dragonfly",
|
||||||
"libc",
|
"libc",
|
||||||
|
@ -1474,6 +1475,15 @@ version = "0.1.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fallible_collections"
|
||||||
|
version = "0.4.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a88c69768c0a15262df21899142bc6df9b9b823546d4b4b9a7bc2d6c448ec6fd"
|
||||||
|
dependencies = [
|
||||||
|
"hashbrown 0.13.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fancy-regex"
|
name = "fancy-regex"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
|
@ -1517,9 +1527,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "2.0.0"
|
version = "2.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764"
|
checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fernet"
|
name = "fernet"
|
||||||
|
@ -1746,6 +1756,16 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gif"
|
||||||
|
version = "0.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045"
|
||||||
|
dependencies = [
|
||||||
|
"color_quant",
|
||||||
|
"weezl",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gimli"
|
name = "gimli"
|
||||||
version = "0.28.0"
|
version = "0.28.0"
|
||||||
|
@ -2431,6 +2451,15 @@ dependencies = [
|
||||||
"ahash 0.7.6",
|
"ahash 0.7.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.13.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
|
||||||
|
dependencies = [
|
||||||
|
"ahash 0.8.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.14.1"
|
version = "0.14.1"
|
||||||
|
@ -2444,21 +2473,20 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashlink"
|
name = "hashlink"
|
||||||
version = "0.8.3"
|
version = "0.8.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "312f66718a2d7789ffef4f4b7b213138ed9f1eb3aa1d0d82fc99f88fb3ffd26f"
|
checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hashbrown 0.14.1",
|
"hashbrown 0.14.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "headers"
|
name = "headers"
|
||||||
version = "0.3.8"
|
version = "0.3.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584"
|
checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.13.1",
|
"base64 0.21.4",
|
||||||
"bitflags 1.3.2",
|
|
||||||
"bytes",
|
"bytes",
|
||||||
"headers-core",
|
"headers-core",
|
||||||
"http",
|
"http",
|
||||||
|
@ -2484,9 +2512,9 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hermit-abi"
|
name = "hermit-abi"
|
||||||
version = "0.3.2"
|
version = "0.3.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
|
checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hex"
|
name = "hex"
|
||||||
|
@ -2671,6 +2699,21 @@ dependencies = [
|
||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "image"
|
||||||
|
version = "0.24.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711"
|
||||||
|
dependencies = [
|
||||||
|
"bytemuck",
|
||||||
|
"byteorder",
|
||||||
|
"color_quant",
|
||||||
|
"gif",
|
||||||
|
"jpeg-decoder",
|
||||||
|
"num-rational 0.4.1",
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "implicit-clone"
|
name = "implicit-clone"
|
||||||
version = "0.3.6"
|
version = "0.3.6"
|
||||||
|
@ -2693,9 +2736,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.0.0"
|
version = "2.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
|
checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown 0.14.1",
|
"hashbrown 0.14.1",
|
||||||
|
@ -2772,6 +2815,12 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jpeg-decoder"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.64"
|
version = "0.3.64"
|
||||||
|
@ -3044,7 +3093,9 @@ dependencies = [
|
||||||
"filetime",
|
"filetime",
|
||||||
"futures",
|
"futures",
|
||||||
"hashbrown 0.14.1",
|
"hashbrown 0.14.1",
|
||||||
|
"hex",
|
||||||
"idlset",
|
"idlset",
|
||||||
|
"image 0.24.7",
|
||||||
"kanidm_build_profiles",
|
"kanidm_build_profiles",
|
||||||
"kanidm_lib_crypto",
|
"kanidm_lib_crypto",
|
||||||
"kanidm_proto",
|
"kanidm_proto",
|
||||||
|
@ -3054,6 +3105,7 @@ dependencies = [
|
||||||
"ldap3_proto",
|
"ldap3_proto",
|
||||||
"libc",
|
"libc",
|
||||||
"libsqlite3-sys",
|
"libsqlite3-sys",
|
||||||
|
"lodepng",
|
||||||
"nonempty",
|
"nonempty",
|
||||||
"num_enum",
|
"num_enum",
|
||||||
"openssl",
|
"openssl",
|
||||||
|
@ -3069,6 +3121,7 @@ dependencies = [
|
||||||
"smartstring",
|
"smartstring",
|
||||||
"smolset",
|
"smolset",
|
||||||
"sshkeys",
|
"sshkeys",
|
||||||
|
"svg",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
|
@ -3289,9 +3342,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.4.5"
|
version = "0.4.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503"
|
checksum = "3852614a3bd9ca9804678ba6be5e3b8ce76dfc902cae004e3e0c44051b6e88db"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
|
@ -3303,6 +3356,19 @@ dependencies = [
|
||||||
"scopeguard",
|
"scopeguard",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lodepng"
|
||||||
|
version = "3.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a3cdccd0cf57a5d456f0656ebcff72c2e19503287e1afbf3b84382812adc0606"
|
||||||
|
dependencies = [
|
||||||
|
"crc32fast",
|
||||||
|
"fallible_collections",
|
||||||
|
"flate2",
|
||||||
|
"libc",
|
||||||
|
"rgb",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.20"
|
version = "0.4.20"
|
||||||
|
@ -3344,9 +3410,9 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchit"
|
name = "matchit"
|
||||||
version = "0.7.2"
|
version = "0.7.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed1202b2a6f884ae56f04cff409ab315c5ce26b5e58d7412e484f01fd52f52ef"
|
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mathru"
|
name = "mathru"
|
||||||
|
@ -3370,9 +3436,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.6.3"
|
version = "2.6.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c"
|
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memmap2"
|
name = "memmap2"
|
||||||
|
@ -3444,6 +3510,24 @@ dependencies = [
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "multer"
|
||||||
|
version = "2.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"encoding_rs",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"httparse",
|
||||||
|
"log",
|
||||||
|
"memchr",
|
||||||
|
"mime",
|
||||||
|
"spin",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "native-tls"
|
name = "native-tls"
|
||||||
version = "0.2.11"
|
version = "0.2.11"
|
||||||
|
@ -3672,9 +3756,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oauth2"
|
name = "oauth2"
|
||||||
version = "4.4.1"
|
version = "4.4.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09a6e2a2b13a56ebeabba9142f911745be6456163fd6c3d361274ebcd891a80c"
|
checksum = "c38841cdd844847e3e7c8d29cef9dcfed8877f8f56f9071f77843ecf3baf937f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.13.1",
|
"base64 0.13.1",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
@ -3684,16 +3768,16 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_path_to_error",
|
"serde_path_to_error",
|
||||||
"sha2 0.10.7",
|
"sha2 0.10.8",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "object"
|
name = "object"
|
||||||
version = "0.32.0"
|
version = "0.32.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "77ac5bbd07aea88c60a577a1ce218075ffd59208b2d7ca97adf9bfc5aeb21ebe"
|
checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
@ -3958,10 +4042,11 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pest"
|
name = "pest"
|
||||||
version = "2.7.2"
|
version = "2.7.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1acb4a4365a13f749a93f1a094a7805e5cfa0955373a9de860d962eaa3a5fe5a"
|
checksum = "c022f1e7b65d6a24c0dbbd5fb344c66881bc01f3e5ae74a1c8100f2f985d98a4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"ucd-trie",
|
"ucd-trie",
|
||||||
]
|
]
|
||||||
|
@ -4096,9 +4181,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prettyplease"
|
name = "prettyplease"
|
||||||
version = "0.2.12"
|
version = "0.2.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6c64d9ba0963cdcea2e1b2230fbae2bab30eb25a174be395c41e764bfb65dd62"
|
checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"syn 2.0.37",
|
"syn 2.0.37",
|
||||||
|
@ -4155,9 +4240,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prodash"
|
name = "prodash"
|
||||||
version = "26.2.1"
|
version = "26.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "50bcc40e3e88402f12b15f94d43a2c7673365e9601cc52795e119b95a266100c"
|
checksum = "794b5bf8e2d19b53dcdcec3e4bba628e20f5b6062503ba89281fa7037dd7bbcf"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prokio"
|
name = "prokio"
|
||||||
|
@ -4199,7 +4284,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "16d2f1455f3630c6e5107b4f2b94e74d76dea80736de0981fd27644216cff57f"
|
checksum = "16d2f1455f3630c6e5107b4f2b94e74d76dea80736de0981fd27644216cff57f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"checked_int_cast",
|
"checked_int_cast",
|
||||||
"image",
|
"image 0.23.14",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -4249,9 +4334,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rayon"
|
name = "rayon"
|
||||||
version = "1.7.0"
|
version = "1.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b"
|
checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"either",
|
"either",
|
||||||
"rayon-core",
|
"rayon-core",
|
||||||
|
@ -4259,14 +4344,12 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rayon-core"
|
name = "rayon-core"
|
||||||
version = "1.11.0"
|
version = "1.12.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d"
|
checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crossbeam-channel",
|
|
||||||
"crossbeam-deque",
|
"crossbeam-deque",
|
||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
"num_cpus",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -4350,9 +4433,9 @@ checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.11.20"
|
version = "0.11.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1"
|
checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-compression",
|
"async-compression",
|
||||||
"base64 0.21.4",
|
"base64 0.21.4",
|
||||||
|
@ -4371,6 +4454,7 @@ dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"mime",
|
"mime",
|
||||||
|
"mime_guess",
|
||||||
"native-tls",
|
"native-tls",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
|
@ -4378,6 +4462,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
|
"system-configuration",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-native-tls",
|
"tokio-native-tls",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
|
@ -4389,6 +4474,15 @@ dependencies = [
|
||||||
"winreg",
|
"winreg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rgb"
|
||||||
|
version = "0.8.36"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "20ec2d3e3fc7a92ced357df9cebd5a10b6fb2aa1ee797bf7e9ce2f17dffc8f59"
|
||||||
|
dependencies = [
|
||||||
|
"bytemuck",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "route-recognizer"
|
name = "route-recognizer"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
|
@ -4478,9 +4572,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "0.38.9"
|
version = "0.38.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9bfe0f2582b4931a45d1fa608f8a8722e8b3c7ac54dd6d5f3b3212791fedef49"
|
checksum = "d2f9da0cbd88f9f09e7814e388301c8414c51c62aa6ce1e4b5c551d49d96e531"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.4.0",
|
"bitflags 2.4.0",
|
||||||
"errno",
|
"errno",
|
||||||
|
@ -4735,7 +4829,7 @@ dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"hex",
|
"hex",
|
||||||
"indexmap 1.9.3",
|
"indexmap 1.9.3",
|
||||||
"indexmap 2.0.0",
|
"indexmap 2.0.2",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_with_macros",
|
"serde_with_macros",
|
||||||
|
@ -4756,9 +4850,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha1"
|
name = "sha1"
|
||||||
version = "0.10.5"
|
version = "0.10.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3"
|
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures",
|
"cpufeatures",
|
||||||
|
@ -4785,9 +4879,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha2"
|
||||||
version = "0.10.7"
|
version = "0.10.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8"
|
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures",
|
"cpufeatures",
|
||||||
|
@ -4796,9 +4890,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sharded-slab"
|
name = "sharded-slab"
|
||||||
version = "0.1.4"
|
version = "0.1.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
|
checksum = "c1b21f559e07218024e7e9f90f96f601825397de0e25420135f7f952453fed0b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
]
|
]
|
||||||
|
@ -4820,9 +4914,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3"
|
checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook"
|
name = "signal-hook"
|
||||||
|
@ -4875,9 +4969,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.11.0"
|
version = "1.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
|
checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
@ -4915,14 +5009,20 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.5.3"
|
version = "0.5.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877"
|
checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "spin"
|
||||||
|
version = "0.9.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sptr"
|
name = "sptr"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
|
@ -4964,6 +5064,12 @@ version = "2.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
|
checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "svg"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "02d815ad337e8449d2374d4248448645edfe74e699343dd5719139d93fa87112"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "1.0.109"
|
version = "1.0.109"
|
||||||
|
@ -5004,6 +5110,27 @@ dependencies = [
|
||||||
"unicode-xid",
|
"unicode-xid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "system-configuration"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"core-foundation",
|
||||||
|
"system-configuration-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "system-configuration-sys"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
|
||||||
|
dependencies = [
|
||||||
|
"core-foundation-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "target-lexicon"
|
name = "target-lexicon"
|
||||||
version = "0.12.11"
|
version = "0.12.11"
|
||||||
|
@ -5034,18 +5161,18 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.47"
|
version = "1.0.49"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f"
|
checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl",
|
"thiserror-impl",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "1.0.47"
|
version = "1.0.49"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b"
|
checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -5084,9 +5211,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.28"
|
version = "0.3.29"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48"
|
checksum = "426f806f4089c493dcac0d24c29c01e2c38baf8e30f1b716ee37e83d200b18fe"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"deranged",
|
"deranged",
|
||||||
"itoa",
|
"itoa",
|
||||||
|
@ -5099,15 +5226,15 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time-core"
|
name = "time-core"
|
||||||
version = "0.1.1"
|
version = "0.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
|
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time-macros"
|
name = "time-macros"
|
||||||
version = "0.2.14"
|
version = "0.2.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572"
|
checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"time-core",
|
"time-core",
|
||||||
]
|
]
|
||||||
|
@ -5150,7 +5277,7 @@ dependencies = [
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
"socket2 0.5.3",
|
"socket2 0.5.4",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
@ -5231,11 +5358,11 @@ checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_edit"
|
name = "toml_edit"
|
||||||
version = "0.19.14"
|
version = "0.19.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a"
|
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap 2.0.0",
|
"indexmap 2.0.2",
|
||||||
"toml_datetime",
|
"toml_datetime",
|
||||||
"winnow",
|
"winnow",
|
||||||
]
|
]
|
||||||
|
@ -5436,9 +5563,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.16.0"
|
version = "1.17.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
|
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ucd-trie"
|
name = "ucd-trie"
|
||||||
|
@ -5469,9 +5596,9 @@ checksum = "98e90c70c9f0d4d1ee6d0a7d04aa06cb9bbd53d8cfbdd62a0269a7c2eb640552"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.11"
|
version = "1.0.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
|
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-normalization"
|
name = "unicode-normalization"
|
||||||
|
@ -5490,9 +5617,9 @@ checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-width"
|
name = "unicode-width"
|
||||||
version = "0.1.10"
|
version = "0.1.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
|
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-xid"
|
name = "unicode-xid"
|
||||||
|
@ -5807,14 +5934,21 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "which"
|
name = "weezl"
|
||||||
version = "4.4.0"
|
version = "0.1.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269"
|
checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "which"
|
||||||
|
version = "4.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"either",
|
"either",
|
||||||
"libc",
|
"home",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"rustix",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -5845,9 +5979,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi-util"
|
name = "winapi-util"
|
||||||
version = "0.1.5"
|
version = "0.1.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
|
checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
20
Cargo.toml
|
@ -88,15 +88,16 @@ argon2 = { version = "0.5.2", features = ["alloc"] }
|
||||||
async-recursion = "1.0.5"
|
async-recursion = "1.0.5"
|
||||||
async-trait = "^0.1.73"
|
async-trait = "^0.1.73"
|
||||||
axum = { version = "0.6.20", features = [
|
axum = { version = "0.6.20", features = [
|
||||||
"json",
|
"form",
|
||||||
"http2",
|
|
||||||
"macros",
|
|
||||||
"tracing",
|
|
||||||
"headers",
|
"headers",
|
||||||
|
"http2",
|
||||||
|
"http2",
|
||||||
|
"json",
|
||||||
|
"macros",
|
||||||
|
"multipart",
|
||||||
"original-uri",
|
"original-uri",
|
||||||
"query",
|
"query",
|
||||||
"form",
|
"tracing",
|
||||||
"http2",
|
|
||||||
] }
|
] }
|
||||||
axum-csp = { version = "0.0.5" }
|
axum-csp = { version = "0.0.5" }
|
||||||
base32 = "^0.4.0"
|
base32 = "^0.4.0"
|
||||||
|
@ -128,6 +129,11 @@ hex = "^0.4.3"
|
||||||
hyper = { version = "0.14.27", features = ["full"] }
|
hyper = { version = "0.14.27", features = ["full"] }
|
||||||
hyper-tls = "0.5.0"
|
hyper-tls = "0.5.0"
|
||||||
idlset = "^0.2.4"
|
idlset = "^0.2.4"
|
||||||
|
image = { version = "0.24.7", default-features = false, features = [
|
||||||
|
"gif",
|
||||||
|
"jpeg",
|
||||||
|
"webp",
|
||||||
|
] }
|
||||||
enum-iterator = "1.4.0"
|
enum-iterator = "1.4.0"
|
||||||
js-sys = "^0.3.63"
|
js-sys = "^0.3.63"
|
||||||
# REMOVE this
|
# REMOVE this
|
||||||
|
@ -138,6 +144,7 @@ ldap3_proto = { version = "^0.3.5", features = ["serde"] }
|
||||||
libc = "^0.2.148"
|
libc = "^0.2.148"
|
||||||
libnss = "^0.4.0"
|
libnss = "^0.4.0"
|
||||||
libsqlite3-sys = "^0.25.0"
|
libsqlite3-sys = "^0.25.0"
|
||||||
|
lodepng = "3.7.2"
|
||||||
lru = "^0.8.0"
|
lru = "^0.8.0"
|
||||||
mathru = "^0.13.0"
|
mathru = "^0.13.0"
|
||||||
notify-debouncer-full = { version = "0.1" }
|
notify-debouncer-full = { version = "0.1" }
|
||||||
|
@ -174,6 +181,7 @@ sketching = { path = "./libs/sketching" }
|
||||||
smartstring = "^1.0.1"
|
smartstring = "^1.0.1"
|
||||||
smolset = "^1.3.1"
|
smolset = "^1.3.1"
|
||||||
sshkeys = "^0.3.1"
|
sshkeys = "^0.3.1"
|
||||||
|
svg = "0.13.1"
|
||||||
syn = { version = "2.0.32", features = ["full"] }
|
syn = { version = "2.0.32", features = ["full"] }
|
||||||
tempfile = "3.8.0"
|
tempfile = "3.8.0"
|
||||||
testkit-macros = { path = "./server/testkit-macros" }
|
testkit-macros = { path = "./server/testkit-macros" }
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
## About these artworks
|
# About these artworks
|
||||||
|
|
||||||
The original artworks were commissioned and produced by Jesse Irwin (tw: @wizardfortress).
|
The original artworks were commissioned and produced by Jesse Irwin (tw: @wizardfortress).
|
||||||
|
|
||||||
The christmas logo was donated and produced by @ateneatla ( https://github.com/ateneatla/ )
|
The christmas logo was donated and produced by [@ateneatla](https://github.com/ateneatla/).
|
||||||
|
|
||||||
The recursive logo was donated and produced by Pi-Cla
|
The recursive logo was donated and produced by Pi-Cla
|
||||||
|
|
||||||
|
|
|
@ -55,7 +55,7 @@ disconnected from the network.
|
||||||
Sudo on workstation
|
Sudo on workstation
|
||||||
===================
|
===================
|
||||||
|
|
||||||
These are re-use of the above two scenarios.
|
These are reuse of the above two scenarios.
|
||||||
|
|
||||||
Access to VPN or Wifi
|
Access to VPN or Wifi
|
||||||
=====================
|
=====================
|
||||||
|
|
|
@ -73,7 +73,7 @@ authenticator for the laptops webauthn:
|
||||||
* (phone) Login to website with password + roaming authenticator
|
* (phone) Login to website with password + roaming authenticator
|
||||||
* (phone) Enroll webauthn for phone SE to account
|
* (phone) Enroll webauthn for phone SE to account
|
||||||
|
|
||||||
While this process does not invole as much fiddling with TOTP, it still has weaknesses.
|
While this process does not involve as much fiddling with TOTP, it still has weaknesses.
|
||||||
|
|
||||||
* The user is expected to own a roaming authenticator capable of working on their phone
|
* The user is expected to own a roaming authenticator capable of working on their phone
|
||||||
* The user is expected to understand different classes of MFA and how they are device bound or not
|
* The user is expected to understand different classes of MFA and how they are device bound or not
|
||||||
|
|
|
@ -189,7 +189,7 @@ or to provide the required information to the remote domain.
|
||||||
We would do a normal auth process, but on determining this is a trust account, we have to return
|
We would do a normal auth process, but on determining this is a trust account, we have to return
|
||||||
a response to the core.rs layer. This should then trigger an async request to domain B which
|
a response to the core.rs layer. This should then trigger an async request to domain B which
|
||||||
contains the request. When this is returned, we then complete the request to the client. This does
|
contains the request. When this is returned, we then complete the request to the client. This does
|
||||||
increase the liklihood of issues or delays in processing in the domain A IO layers if many requests
|
increase the likelihood of issues or delays in processing in the domain A IO layers if many requests
|
||||||
exist at the same time.
|
exist at the same time.
|
||||||
|
|
||||||
if multiple urls exist in the trustanchor, we should choose randomly which to contact for
|
if multiple urls exist in the trustanchor, we should choose randomly which to contact for
|
||||||
|
|
|
@ -76,7 +76,7 @@ already with our client authorisation checks. This is discussed in
|
||||||
In this design we associate a "not issued before" (NIB) timestamp to our sessions. For a refresh
|
In this design we associate a "not issued before" (NIB) timestamp to our sessions. For a refresh
|
||||||
token to be valid for issuance, the refresh tokens IAT must be greater than or equal to the NIB.
|
token to be valid for issuance, the refresh tokens IAT must be greater than or equal to the NIB.
|
||||||
|
|
||||||
In this example were the refresh token with IAT 1 re-used after the second token was issued, then
|
In this example were the refresh token with IAT 1 reused after the second token was issued, then
|
||||||
this condition would fail as the NIB has advanced to 2. Since IAT 1 is not greater or equal to NIB 2
|
this condition would fail as the NIB has advanced to 2. Since IAT 1 is not greater or equal to NIB 2
|
||||||
then the refresh token _must_ have previously been used for access token exchange.
|
then the refresh token _must_ have previously been used for access token exchange.
|
||||||
|
|
||||||
|
@ -132,7 +132,7 @@ hinders the ability to attack this for very little gain.
|
||||||
## Attack Detection
|
## Attack Detection
|
||||||
|
|
||||||
[draft oauth security topics section 4.14.2](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.14.2)
|
[draft oauth security topics section 4.14.2](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.14.2)
|
||||||
specifically calls out that when refresh token re-use is detected then all tokens of the session
|
specifically calls out that when refresh token reuse is detected then all tokens of the session
|
||||||
should be canceled to cause a new authorisation code flow to be initiated.
|
should be canceled to cause a new authorisation code flow to be initiated.
|
||||||
|
|
||||||
## Inactive Refresh Tokens
|
## Inactive Refresh Tokens
|
||||||
|
@ -149,7 +149,7 @@ consistency plugin that already exists.
|
||||||
Since the act of refreshing a token is implied activity then we do not require other signaling
|
Since the act of refreshing a token is implied activity then we do not require other signaling
|
||||||
mechanisms.
|
mechanisms.
|
||||||
|
|
||||||
# Questions
|
## Questions
|
||||||
|
|
||||||
Currently with authorisation code grants and sessions we issue these where the sessions are recorded
|
Currently with authorisation code grants and sessions we issue these where the sessions are recorded
|
||||||
in an async manner. For consistency I believe the same should be true here but is there a concern
|
in an async manner. For consistency I believe the same should be true here but is there a concern
|
||||||
|
|
|
@ -26,7 +26,7 @@ An option is to scan the filter for and Eq(class, deleted) terms, and if present
|
||||||
and operation.
|
and operation.
|
||||||
|
|
||||||
A possibly better option is that filter constructors should have two constructors. One that
|
A possibly better option is that filter constructors should have two constructors. One that
|
||||||
adds the wrapping AndNot term, and one that does not. This way the plugin implementor only
|
adds the wrapping AndNot term, and one that does not. This way the plugin implementer only
|
||||||
needs to construct from the correct call, and they would exclude / include recycled items. This
|
needs to construct from the correct call, and they would exclude / include recycled items. This
|
||||||
also would allow externally supplied filters to be correctly wrapped. The main consideration here
|
also would allow externally supplied filters to be correctly wrapped. The main consideration here
|
||||||
is that it would require another api endpoint allowing recycle-bin searches. This is probably not
|
is that it would require another api endpoint allowing recycle-bin searches. This is probably not
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
## Frequently Asked Questions
|
# Frequently Asked Questions
|
||||||
|
|
||||||
This is a list of common questions that are generally raised by developers or technical users.
|
This is a list of common questions that are generally raised by developers or technical users.
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ parts. This creates production fragility and issues such as:
|
||||||
This last point is key. It is a critical part of kanidm that the following must work on all
|
This last point is key. It is a critical part of kanidm that the following must work on all
|
||||||
machines, and run every single test in the suite.
|
machines, and run every single test in the suite.
|
||||||
|
|
||||||
```
|
```shell
|
||||||
git clone https://github.com/kanidm/kanidm.git
|
git clone https://github.com/kanidm/kanidm.git
|
||||||
cd kanidm
|
cd kanidm
|
||||||
cargo test
|
cargo test
|
||||||
|
@ -46,7 +46,7 @@ where it would not be possible to effectively test for all developers.
|
||||||
## Why don't you use Raft/Etcd/MongoDB/Other to solve replication?
|
## Why don't you use Raft/Etcd/MongoDB/Other to solve replication?
|
||||||
|
|
||||||
There are a number of reasons why these are generally not compatible. Generally these databases or
|
There are a number of reasons why these are generally not compatible. Generally these databases or
|
||||||
technolgies do solve problems, but they are not the problems in Kanidm.
|
technologies do solve problems, but they are not the problems in Kanidm.
|
||||||
|
|
||||||
## CAP theorem
|
## CAP theorem
|
||||||
|
|
||||||
|
@ -105,7 +105,7 @@ Name Service Switch (NSS) is used for connecting the computers with different da
|
||||||
resolve name-service information. By adding the nsswitch libraries to /etc/nsswitch.conf, we are
|
resolve name-service information. By adding the nsswitch libraries to /etc/nsswitch.conf, we are
|
||||||
telling NSS to lookup password info and group identities in Kanidm:
|
telling NSS to lookup password info and group identities in Kanidm:
|
||||||
|
|
||||||
```
|
```text
|
||||||
passwd: compat kanidm
|
passwd: compat kanidm
|
||||||
group: compat kanidm
|
group: compat kanidm
|
||||||
```
|
```
|
||||||
|
|
|
@ -88,7 +88,7 @@ To configure Kanidm to provide LDAP, add the argument to the `server.toml` confi
|
||||||
ldapbindaddress = "127.0.0.1:3636"
|
ldapbindaddress = "127.0.0.1:3636"
|
||||||
```
|
```
|
||||||
|
|
||||||
You should configure TLS certificates and keys as usual - LDAP will re-use the Web server TLS
|
You should configure TLS certificates and keys as usual - LDAP will reuse the Web server TLS
|
||||||
material.
|
material.
|
||||||
|
|
||||||
## Showing LDAP Entries and Attribute Maps
|
## Showing LDAP Entries and Attribute Maps
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# Monitoring the platform
|
# Monitoring the platform
|
||||||
|
|
||||||
The monitoring design of Kanidm is still very much in its infancy -
|
The monitoring design of Kanidm is still very much in its infancy -
|
||||||
[take part in the dicussion at github.com/kanidm/kanidm/issues/216](https://github.com/kanidm/kanidm/issues/216).
|
[take part in the discussion at github.com/kanidm/kanidm/issues/216](https://github.com/kanidm/kanidm/issues/216).
|
||||||
|
|
||||||
## kanidmd
|
## kanidmd
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,9 @@ repository = { workspace = true }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
reqwest = { workspace = true, default-features = false }
|
reqwest = { workspace = true, default-features = false, features = [
|
||||||
|
"multipart",
|
||||||
|
] }
|
||||||
kanidm_proto = { workspace = true }
|
kanidm_proto = { workspace = true }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
|
|
@ -27,6 +27,7 @@ use std::time::Duration;
|
||||||
use kanidm_proto::constants::{APPLICATION_JSON, ATTR_NAME};
|
use kanidm_proto::constants::{APPLICATION_JSON, ATTR_NAME};
|
||||||
use kanidm_proto::v1::*;
|
use kanidm_proto::v1::*;
|
||||||
use reqwest::header::CONTENT_TYPE;
|
use reqwest::header::CONTENT_TYPE;
|
||||||
|
use reqwest::Response;
|
||||||
pub use reqwest::StatusCode;
|
pub use reqwest::StatusCode;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -561,7 +562,7 @@ impl KanidmClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// You've got the response from a reqwest and you want to turn it into a `ClientError`
|
/// You've got the response from a reqwest and you want to turn it into a `ClientError`
|
||||||
fn handle_response_error(&self, error: reqwest::Error) -> ClientError {
|
pub fn handle_response_error(&self, error: reqwest::Error) -> ClientError {
|
||||||
if error.is_connect() {
|
if error.is_connect() {
|
||||||
if find_reqwest_error_source::<std::io::Error>(&error).is_some() {
|
if find_reqwest_error_source::<std::io::Error>(&error).is_some() {
|
||||||
// TODO: one day handle IO errors better
|
// TODO: one day handle IO errors better
|
||||||
|
@ -582,6 +583,18 @@ impl KanidmClient {
|
||||||
ClientError::Transport(error)
|
ClientError::Transport(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_kopid_from_response(&self, response: &Response) -> String {
|
||||||
|
let opid = response
|
||||||
|
.headers()
|
||||||
|
.get(KOPID)
|
||||||
|
.and_then(|hv| hv.to_str().ok())
|
||||||
|
.unwrap_or("missing_kopid")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
debug!("opid -> {:?}", opid);
|
||||||
|
opid
|
||||||
|
}
|
||||||
|
|
||||||
async fn perform_simple_post_request<R: Serialize, T: DeserializeOwned>(
|
async fn perform_simple_post_request<R: Serialize, T: DeserializeOwned>(
|
||||||
&self,
|
&self,
|
||||||
dest: &str,
|
dest: &str,
|
||||||
|
@ -602,13 +615,7 @@ impl KanidmClient {
|
||||||
|
|
||||||
self.expect_version(&response).await;
|
self.expect_version(&response).await;
|
||||||
|
|
||||||
let opid = response
|
let opid = self.get_kopid_from_response(&response);
|
||||||
.headers()
|
|
||||||
.get(KOPID)
|
|
||||||
.and_then(|hv| hv.to_str().ok())
|
|
||||||
.unwrap_or("missing_kopid")
|
|
||||||
.to_string();
|
|
||||||
debug!("opid -> {:?}", opid);
|
|
||||||
|
|
||||||
match response.status() {
|
match response.status() {
|
||||||
reqwest::StatusCode::OK => {}
|
reqwest::StatusCode::OK => {}
|
||||||
|
@ -679,12 +686,7 @@ impl KanidmClient {
|
||||||
.and_then(|hv| hv.to_str().ok().map(str::to_string));
|
.and_then(|hv| hv.to_str().ok().map(str::to_string));
|
||||||
}
|
}
|
||||||
|
|
||||||
let opid = headers
|
let opid = self.get_kopid_from_response(&response);
|
||||||
.get(KOPID)
|
|
||||||
.and_then(|hv| hv.to_str().ok())
|
|
||||||
.unwrap_or("missing_kopid")
|
|
||||||
.to_string();
|
|
||||||
debug!("opid -> {:?}", opid);
|
|
||||||
|
|
||||||
match response.status() {
|
match response.status() {
|
||||||
reqwest::StatusCode::OK => {}
|
reqwest::StatusCode::OK => {}
|
||||||
|
@ -731,13 +733,7 @@ impl KanidmClient {
|
||||||
|
|
||||||
self.expect_version(&response).await;
|
self.expect_version(&response).await;
|
||||||
|
|
||||||
let opid = response
|
let opid = self.get_kopid_from_response(&response);
|
||||||
.headers()
|
|
||||||
.get(KOPID)
|
|
||||||
.and_then(|hv| hv.to_str().ok())
|
|
||||||
.unwrap_or("missing_kopid")
|
|
||||||
.to_string();
|
|
||||||
debug!("opid -> {:?}", opid);
|
|
||||||
|
|
||||||
match response.status() {
|
match response.status() {
|
||||||
reqwest::StatusCode::OK => {}
|
reqwest::StatusCode::OK => {}
|
||||||
|
@ -784,14 +780,7 @@ impl KanidmClient {
|
||||||
|
|
||||||
self.expect_version(&response).await;
|
self.expect_version(&response).await;
|
||||||
|
|
||||||
let opid = response
|
let opid = self.get_kopid_from_response(&response);
|
||||||
.headers()
|
|
||||||
.get(KOPID)
|
|
||||||
.and_then(|hv| hv.to_str().ok())
|
|
||||||
.unwrap_or("missing_kopid")
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
debug!("opid -> {:?}", opid);
|
|
||||||
|
|
||||||
match response.status() {
|
match response.status() {
|
||||||
reqwest::StatusCode::OK => {}
|
reqwest::StatusCode::OK => {}
|
||||||
|
@ -838,13 +827,7 @@ impl KanidmClient {
|
||||||
|
|
||||||
self.expect_version(&response).await;
|
self.expect_version(&response).await;
|
||||||
|
|
||||||
let opid = response
|
let opid = self.get_kopid_from_response(&response);
|
||||||
.headers()
|
|
||||||
.get(KOPID)
|
|
||||||
.and_then(|hv| hv.to_str().ok())
|
|
||||||
.unwrap_or("missing_kopid")
|
|
||||||
.to_string();
|
|
||||||
debug!("opid -> {:?}", opid);
|
|
||||||
|
|
||||||
match response.status() {
|
match response.status() {
|
||||||
reqwest::StatusCode::OK => {}
|
reqwest::StatusCode::OK => {}
|
||||||
|
@ -885,14 +868,7 @@ impl KanidmClient {
|
||||||
|
|
||||||
self.expect_version(&response).await;
|
self.expect_version(&response).await;
|
||||||
|
|
||||||
let opid = response
|
let opid = self.get_kopid_from_response(&response);
|
||||||
.headers()
|
|
||||||
.get(KOPID)
|
|
||||||
.and_then(|hv| hv.to_str().ok())
|
|
||||||
.unwrap_or("missing_kopid")
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
debug!("opid -> {:?}", opid);
|
|
||||||
|
|
||||||
match response.status() {
|
match response.status() {
|
||||||
reqwest::StatusCode::OK => {}
|
reqwest::StatusCode::OK => {}
|
||||||
|
@ -933,13 +909,7 @@ impl KanidmClient {
|
||||||
|
|
||||||
self.expect_version(&response).await;
|
self.expect_version(&response).await;
|
||||||
|
|
||||||
let opid = response
|
let opid = self.get_kopid_from_response(&response);
|
||||||
.headers()
|
|
||||||
.get(KOPID)
|
|
||||||
.and_then(|hv| hv.to_str().ok())
|
|
||||||
.unwrap_or("missing_kopid")
|
|
||||||
.to_string();
|
|
||||||
debug!("opid -> {:?}", opid);
|
|
||||||
|
|
||||||
match response.status() {
|
match response.status() {
|
||||||
reqwest::StatusCode::OK => {}
|
reqwest::StatusCode::OK => {}
|
||||||
|
@ -986,13 +956,7 @@ impl KanidmClient {
|
||||||
|
|
||||||
self.expect_version(&response).await;
|
self.expect_version(&response).await;
|
||||||
|
|
||||||
let opid = response
|
let opid = self.get_kopid_from_response(&response);
|
||||||
.headers()
|
|
||||||
.get(KOPID)
|
|
||||||
.and_then(|hv| hv.to_str().ok())
|
|
||||||
.unwrap_or("missing_kopid")
|
|
||||||
.to_string();
|
|
||||||
debug!("opid -> {:?}", opid);
|
|
||||||
|
|
||||||
match response.status() {
|
match response.status() {
|
||||||
reqwest::StatusCode::OK => {}
|
reqwest::StatusCode::OK => {}
|
||||||
|
@ -1459,14 +1423,7 @@ impl KanidmClient {
|
||||||
|
|
||||||
self.expect_version(&response).await;
|
self.expect_version(&response).await;
|
||||||
|
|
||||||
let opid = response
|
let opid = self.get_kopid_from_response(&response);
|
||||||
.headers()
|
|
||||||
.get(KOPID)
|
|
||||||
.and_then(|hv| hv.to_str().ok())
|
|
||||||
.unwrap_or("missing_kopid")
|
|
||||||
.to_string();
|
|
||||||
debug!("opid -> {:?}", opid);
|
|
||||||
|
|
||||||
match response.status() {
|
match response.status() {
|
||||||
// Continue to process.
|
// Continue to process.
|
||||||
reqwest::StatusCode::OK => {}
|
reqwest::StatusCode::OK => {}
|
||||||
|
|
|
@ -3,7 +3,9 @@ use kanidm_proto::constants::{
|
||||||
ATTR_DISPLAYNAME, ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE,
|
ATTR_DISPLAYNAME, ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE,
|
||||||
ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE, ATTR_OAUTH2_RS_NAME, ATTR_OAUTH2_RS_ORIGIN,
|
ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE, ATTR_OAUTH2_RS_NAME, ATTR_OAUTH2_RS_ORIGIN,
|
||||||
};
|
};
|
||||||
|
use kanidm_proto::internal::ImageValue;
|
||||||
use kanidm_proto::v1::Entry;
|
use kanidm_proto::v1::Entry;
|
||||||
|
use reqwest::multipart;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
impl KanidmClient {
|
impl KanidmClient {
|
||||||
|
@ -179,6 +181,74 @@ impl KanidmClient {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Want to delete the image associated with a resource server? Here's your thing!
|
||||||
|
pub async fn idm_oauth2_rs_delete_image(&self, id: &str) -> Result<(), ClientError> {
|
||||||
|
self.perform_delete_request(format!("/v1/oauth2/{}/_image", id).as_str())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Want to add/update the image associated with a resource server? Here's your thing!
|
||||||
|
pub async fn idm_oauth2_rs_update_image(
|
||||||
|
&self,
|
||||||
|
id: &str,
|
||||||
|
image: ImageValue,
|
||||||
|
) -> Result<(), ClientError> {
|
||||||
|
let file_content_type = image.filetype.as_content_type_str();
|
||||||
|
|
||||||
|
let file_data = match multipart::Part::bytes(image.contents.clone())
|
||||||
|
.file_name(image.filename)
|
||||||
|
.mime_str(file_content_type)
|
||||||
|
{
|
||||||
|
Ok(part) => part,
|
||||||
|
Err(err) => {
|
||||||
|
error!(
|
||||||
|
"Failed to generate multipart body from image data: {:}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
return Err(ClientError::SystemError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let form = multipart::Form::new().part("image", file_data);
|
||||||
|
|
||||||
|
// send it
|
||||||
|
let response = self
|
||||||
|
.client
|
||||||
|
.post(self.make_url(&format!("/v1/oauth2/{}/_image", id)))
|
||||||
|
.multipart(form);
|
||||||
|
|
||||||
|
let response = {
|
||||||
|
let tguard = self.bearer_token.read().await;
|
||||||
|
if let Some(token) = &(*tguard) {
|
||||||
|
response.bearer_auth(token)
|
||||||
|
} else {
|
||||||
|
response
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let response = response
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|err| self.handle_response_error(err))?;
|
||||||
|
self.expect_version(&response).await;
|
||||||
|
|
||||||
|
let opid = self.get_kopid_from_response(&response);
|
||||||
|
|
||||||
|
match response.status() {
|
||||||
|
reqwest::StatusCode::OK => {}
|
||||||
|
unexpect => {
|
||||||
|
return Err(ClientError::Http(
|
||||||
|
unexpect,
|
||||||
|
response.json().await.ok(),
|
||||||
|
opid,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.map_err(|e| ClientError::JsonDecode(e, opid))
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn idm_oauth2_rs_enable_pkce(&self, id: &str) -> Result<(), ClientError> {
|
pub async fn idm_oauth2_rs_enable_pkce(&self, id: &str) -> Result<(), ClientError> {
|
||||||
let mut update_oauth2_rs = Entry {
|
let mut update_oauth2_rs = Entry {
|
||||||
attrs: BTreeMap::new(),
|
attrs: BTreeMap::new(),
|
||||||
|
|
|
@ -38,12 +38,6 @@ pub fn readonly(meta: &Metadata) -> bool {
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
#[test]
|
#[test]
|
||||||
fn test_readonly() {
|
fn test_readonly() {
|
||||||
// check if the file Cargo.toml exists
|
|
||||||
use std::path::Path;
|
|
||||||
if Path::new("Cargo.toml").exists() == false {
|
|
||||||
panic!("Can't find Cargo.toml");
|
|
||||||
}
|
|
||||||
|
|
||||||
let meta = std::fs::metadata("Cargo.toml").expect("Can't find Cargo.toml");
|
let meta = std::fs::metadata("Cargo.toml").expect("Can't find Cargo.toml");
|
||||||
println!("meta={:?} -> readonly={:?}", meta, readonly(&meta));
|
println!("meta={:?} -> readonly={:?}", meta, readonly(&meta));
|
||||||
assert!(readonly(&meta) == false);
|
assert!(readonly(&meta) == false);
|
||||||
|
|
|
@ -1,4 +1,19 @@
|
||||||
/// Because consistency is great!
|
//! Because consistency is great!
|
||||||
|
|
||||||
|
pub const CONTENT_TYPE_JPG: &str = "image/jpeg";
|
||||||
|
pub const CONTENT_TYPE_PNG: &str = "image/png";
|
||||||
|
pub const CONTENT_TYPE_GIF: &str = "image/gif";
|
||||||
|
pub const CONTENT_TYPE_SVG: &str = "image/svg+xml";
|
||||||
|
pub const CONTENT_TYPE_WEBP: &str = "image/webp";
|
||||||
|
|
||||||
|
// for when the user uploads things to the various image endpoints
|
||||||
|
pub const VALID_IMAGE_UPLOAD_CONTENT_TYPES: [&str; 5] = [
|
||||||
|
CONTENT_TYPE_JPG,
|
||||||
|
CONTENT_TYPE_PNG,
|
||||||
|
CONTENT_TYPE_GIF,
|
||||||
|
CONTENT_TYPE_SVG,
|
||||||
|
CONTENT_TYPE_WEBP,
|
||||||
|
];
|
||||||
|
|
||||||
pub const APPLICATION_JSON: &str = "application/json";
|
pub const APPLICATION_JSON: &str = "application/json";
|
||||||
|
|
||||||
|
@ -68,6 +83,7 @@ pub const ATTR_GIDNUMBER: &str = "gidnumber";
|
||||||
pub const ATTR_GRANT_UI_HINT: &str = "grant_ui_hint";
|
pub const ATTR_GRANT_UI_HINT: &str = "grant_ui_hint";
|
||||||
pub const ATTR_GROUP: &str = "group";
|
pub const ATTR_GROUP: &str = "group";
|
||||||
pub const ATTR_ID_VERIFICATION_ECKEY: &str = "id_verification_eckey";
|
pub const ATTR_ID_VERIFICATION_ECKEY: &str = "id_verification_eckey";
|
||||||
|
pub const ATTR_IMAGE: &str = "image";
|
||||||
pub const ATTR_INDEX: &str = "index";
|
pub const ATTR_INDEX: &str = "index";
|
||||||
pub const ATTR_IPANTHASH: &str = "ipanthash";
|
pub const ATTR_IPANTHASH: &str = "ipanthash";
|
||||||
pub const ATTR_IPASSHPUBKEY: &str = "ipasshpubkey";
|
pub const ATTR_IPASSHPUBKEY: &str = "ipasshpubkey";
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
use crate::constants::{
|
||||||
|
CONTENT_TYPE_GIF, CONTENT_TYPE_JPG, CONTENT_TYPE_PNG, CONTENT_TYPE_SVG, CONTENT_TYPE_WEBP,
|
||||||
|
};
|
||||||
use crate::v1::ApiTokenPurpose;
|
use crate::v1::ApiTokenPurpose;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
@ -45,3 +48,83 @@ pub enum IdentifyUserResponse {
|
||||||
CodeFailure,
|
CodeFailure,
|
||||||
InvalidUserId,
|
InvalidUserId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash, Ord, PartialOrd)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum ImageType {
|
||||||
|
Png,
|
||||||
|
Jpg,
|
||||||
|
Gif,
|
||||||
|
Svg,
|
||||||
|
Webp,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&str> for ImageType {
|
||||||
|
type Error = &'static str;
|
||||||
|
/// ```
|
||||||
|
/// use kanidm_proto::internal::ImageType;
|
||||||
|
/// assert_eq!(ImageType::try_from("png").unwrap(), ImageType::Png);
|
||||||
|
/// assert!(ImageType::try_from("krabs").is_err());
|
||||||
|
/// ```
|
||||||
|
fn try_from(value: &str) -> Result<Self, &'static str> {
|
||||||
|
#[allow(clippy::panic)]
|
||||||
|
match value {
|
||||||
|
"png" => Ok(Self::Png),
|
||||||
|
"jpg" => Ok(Self::Jpg),
|
||||||
|
"jpeg" => Ok(Self::Jpg), // ugh I hate this
|
||||||
|
"gif" => Ok(Self::Gif),
|
||||||
|
"svg" => Ok(Self::Svg),
|
||||||
|
"webp" => Ok(Self::Webp),
|
||||||
|
_ => Err("Invalid image type!"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImageType {
|
||||||
|
pub fn try_from_content_type(content_type: &str) -> Result<Self, String> {
|
||||||
|
let content_type = content_type.to_lowercase();
|
||||||
|
match content_type.as_str() {
|
||||||
|
CONTENT_TYPE_JPG => Ok(ImageType::Jpg),
|
||||||
|
CONTENT_TYPE_PNG => Ok(ImageType::Png),
|
||||||
|
CONTENT_TYPE_GIF => Ok(ImageType::Gif),
|
||||||
|
CONTENT_TYPE_WEBP => Ok(ImageType::Webp),
|
||||||
|
CONTENT_TYPE_SVG => Ok(ImageType::Svg),
|
||||||
|
_ => Err(format!("Invalid content type: {}", content_type)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_content_type_str(&self) -> &'static str {
|
||||||
|
match &self {
|
||||||
|
ImageType::Jpg => CONTENT_TYPE_JPG,
|
||||||
|
ImageType::Png => CONTENT_TYPE_PNG,
|
||||||
|
ImageType::Gif => CONTENT_TYPE_GIF,
|
||||||
|
ImageType::Webp => CONTENT_TYPE_WEBP,
|
||||||
|
ImageType::Svg => CONTENT_TYPE_SVG,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct ImageValue {
|
||||||
|
pub filename: String,
|
||||||
|
pub filetype: ImageType,
|
||||||
|
pub contents: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&str> for ImageValue {
|
||||||
|
type Error = String;
|
||||||
|
fn try_from(s: &str) -> Result<Self, String> {
|
||||||
|
serde_json::from_str(s)
|
||||||
|
.map_err(|e| format!("Failed to decode ImageValue from {} - {:?}", s, e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImageValue {
|
||||||
|
pub fn new(filename: String, filetype: ImageType, contents: Vec<u8>) -> Self {
|
||||||
|
Self {
|
||||||
|
filename,
|
||||||
|
filetype,
|
||||||
|
contents,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ use std::net::IpAddr;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use kanidm_proto::internal::{AppLink, IdentifyUserRequest, IdentifyUserResponse};
|
use kanidm_proto::internal::{AppLink, IdentifyUserRequest, IdentifyUserResponse, ImageValue};
|
||||||
use kanidm_proto::v1::{
|
use kanidm_proto::v1::{
|
||||||
ApiToken, AuthIssueSession, AuthRequest, BackupCodesView, CURequest, CUSessionToken, CUStatus,
|
ApiToken, AuthIssueSession, AuthRequest, BackupCodesView, CURequest, CUSessionToken, CUStatus,
|
||||||
CredentialStatus, Entry as ProtoEntry, OperationError, RadiusAuthToken, SearchRequest,
|
CredentialStatus, Entry as ProtoEntry, OperationError, RadiusAuthToken, SearchRequest,
|
||||||
|
@ -402,6 +402,45 @@ impl QueryServerReadV1 {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip_all)]
|
||||||
|
/// pull an image so we can present it to the user
|
||||||
|
pub async fn handle_oauth2_rs_image_get_image(
|
||||||
|
&self,
|
||||||
|
uat: Option<String>,
|
||||||
|
rs: Filter<FilterInvalid>,
|
||||||
|
) -> Result<ImageValue, OperationError> {
|
||||||
|
let mut idms_prox_read = self.idms.proxy_read().await;
|
||||||
|
let ct = duration_from_epoch_now();
|
||||||
|
|
||||||
|
let ident = idms_prox_read
|
||||||
|
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||||
|
.map_err(|e| {
|
||||||
|
admin_error!(err = ?e, "Invalid identity in handle_oauth2_rs_image_get_image {:?}", uat);
|
||||||
|
e
|
||||||
|
})?;
|
||||||
|
let attrs = vec![Attribute::Image.to_string()];
|
||||||
|
|
||||||
|
let search = SearchEvent::from_internal_message(
|
||||||
|
ident,
|
||||||
|
&rs,
|
||||||
|
Some(attrs.as_slice()),
|
||||||
|
&mut idms_prox_read.qs_read,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let entries = idms_prox_read.qs_read.search(&search)?;
|
||||||
|
if entries.is_empty() {
|
||||||
|
return Err(OperationError::NoMatchingEntries);
|
||||||
|
}
|
||||||
|
let entry = match entries.first() {
|
||||||
|
Some(entry) => entry,
|
||||||
|
None => return Err(OperationError::NoMatchingEntries),
|
||||||
|
};
|
||||||
|
match entry.get_ava_single_image(Attribute::Image) {
|
||||||
|
Some(image) => Ok(image),
|
||||||
|
None => Err(OperationError::NoMatchingEntries),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(
|
#[instrument(
|
||||||
level = "info",
|
level = "info",
|
||||||
skip_all,
|
skip_all,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use std::time::Duration;
|
|
||||||
use std::{iter, sync::Arc};
|
use std::{iter, sync::Arc};
|
||||||
|
|
||||||
|
use kanidm_proto::internal::ImageValue;
|
||||||
use kanidm_proto::v1::{
|
use kanidm_proto::v1::{
|
||||||
AccountUnixExtend, CUIntentToken, CUSessionToken, CUStatus, CreateRequest, DeleteRequest,
|
AccountUnixExtend, CUIntentToken, CUSessionToken, CUStatus, CreateRequest, DeleteRequest,
|
||||||
Entry as ProtoEntry, GroupUnixExtend, Modify as ProtoModify, ModifyList as ProtoModifyList,
|
Entry as ProtoEntry, GroupUnixExtend, Modify as ProtoModify, ModifyList as ProtoModifyList,
|
||||||
|
@ -88,7 +88,7 @@ impl QueryServerWriteV1 {
|
||||||
) {
|
) {
|
||||||
Ok(m) => m,
|
Ok(m) => m,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
admin_error!(err=?e, "Failed to begin modify");
|
admin_error!(err=?e, "Failed to begin modify during modify_from_parts");
|
||||||
return Err(e);
|
return Err(e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -139,7 +139,7 @@ impl QueryServerWriteV1 {
|
||||||
) {
|
) {
|
||||||
Ok(m) => m,
|
Ok(m) => m,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
admin_error!(err = ?e, "Failed to begin modify");
|
admin_error!(err = ?e, "Failed to begin modify during modify_from_internal_parts");
|
||||||
return Err(e);
|
return Err(e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -212,7 +212,7 @@ impl QueryServerWriteV1 {
|
||||||
let mdf = match ModifyEvent::from_message(ident, &req, &mut idms_prox_write.qs_write) {
|
let mdf = match ModifyEvent::from_message(ident, &req, &mut idms_prox_write.qs_write) {
|
||||||
Ok(m) => m,
|
Ok(m) => m,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
admin_error!(err = ?e, "Failed to begin modify");
|
admin_error!(err = ?e, "Failed to begin modify during handle_modify");
|
||||||
return Err(e);
|
return Err(e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -292,7 +292,7 @@ impl QueryServerWriteV1 {
|
||||||
let mdf =
|
let mdf =
|
||||||
ModifyEvent::from_internal_parts(ident, &modlist, &filter, &idms_prox_write.qs_write)
|
ModifyEvent::from_internal_parts(ident, &modlist, &filter, &idms_prox_write.qs_write)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
admin_error!(err = ?e, "Failed to begin modify");
|
admin_error!(err = ?e, "Failed to begin modify during handle_internalpatch");
|
||||||
e
|
e
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
@ -892,7 +892,7 @@ impl QueryServerWriteV1 {
|
||||||
) {
|
) {
|
||||||
Ok(m) => m,
|
Ok(m) => m,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
admin_error!(err = ?e, "Failed to begin modify");
|
admin_error!(err = ?e, "Failed to begin modify during purge attribute");
|
||||||
return Err(e);
|
return Err(e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1181,6 +1181,72 @@ impl QueryServerWriteV1 {
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip_all)]
|
||||||
|
pub async fn handle_oauth2_rs_image_delete(
|
||||||
|
&self,
|
||||||
|
uat: Option<String>,
|
||||||
|
rs: Filter<FilterInvalid>,
|
||||||
|
) -> Result<(), OperationError> {
|
||||||
|
let mut idms_prox_write = self.idms.proxy_write(duration_from_epoch_now()).await;
|
||||||
|
let ct = duration_from_epoch_now();
|
||||||
|
|
||||||
|
let ident = idms_prox_write
|
||||||
|
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||||
|
.map_err(|e| {
|
||||||
|
admin_error!(err = ?e, "Invalid identity in handle_oauth2_rs_image_delete {:?}", uat);
|
||||||
|
e
|
||||||
|
})?;
|
||||||
|
let ml = ModifyList::new_purge(Attribute::Image);
|
||||||
|
let mdf = match ModifyEvent::from_internal_parts(ident, &ml, &rs, &idms_prox_write.qs_write)
|
||||||
|
{
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(e) => {
|
||||||
|
admin_error!(err = ?e, "Failed to begin modify during handle_oauth2_rs_image_delete");
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
idms_prox_write
|
||||||
|
.qs_write
|
||||||
|
.modify(&mdf)
|
||||||
|
.and_then(|_| idms_prox_write.commit().map(|_| ()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip_all)]
|
||||||
|
pub async fn handle_oauth2_rs_image_update(
|
||||||
|
&self,
|
||||||
|
uat: Option<String>,
|
||||||
|
rs: Filter<FilterInvalid>,
|
||||||
|
image: ImageValue,
|
||||||
|
) -> Result<(), OperationError> {
|
||||||
|
let mut idms_prox_write = self.idms.proxy_write(duration_from_epoch_now()).await;
|
||||||
|
let ct = duration_from_epoch_now();
|
||||||
|
|
||||||
|
let ident = idms_prox_write
|
||||||
|
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||||
|
.map_err(|e| {
|
||||||
|
admin_error!(err = ?e, "Invalid identity in handle_oauth2_rs_image_update {:?}", uat);
|
||||||
|
e
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let ml = ModifyList::new_purge_and_set(Attribute::Image, Value::Image(image));
|
||||||
|
|
||||||
|
let mdf = match ModifyEvent::from_internal_parts(ident, &ml, &rs, &idms_prox_write.qs_write)
|
||||||
|
{
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(e) => {
|
||||||
|
admin_error!(err = ?e, "Failed to begin modify during handle_oauth2_rs_image_update");
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
trace!(?mdf, "Begin modify event");
|
||||||
|
|
||||||
|
idms_prox_write
|
||||||
|
.qs_write
|
||||||
|
.modify(&mdf)
|
||||||
|
.and_then(|_| idms_prox_write.commit().map(|_| ()))
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(
|
#[instrument(
|
||||||
level = "info",
|
level = "info",
|
||||||
skip_all,
|
skip_all,
|
||||||
|
|
|
@ -208,7 +208,8 @@ pub async fn create_https_server(
|
||||||
.route("/", get(|| async { Redirect::temporary("/ui") }))
|
.route("/", get(|| async { Redirect::temporary("/ui") }))
|
||||||
.route("/manifest.webmanifest", get(manifest::manifest))
|
.route("/manifest.webmanifest", get(manifest::manifest))
|
||||||
.nest("/ui", spa_router)
|
.nest("/ui", spa_router)
|
||||||
.layer(middleware::compression::new()) // TODO: this needs to be configured properly
|
.layer(middleware::compression::new())
|
||||||
|
.route("/ui/images/oauth2/:rs_name", get(oauth2::oauth2_image_get))
|
||||||
}
|
}
|
||||||
ServerRole::WriteReplicaNoUI => Router::new(),
|
ServerRole::WriteReplicaNoUI => Router::new(),
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,6 +14,7 @@ use http::header::{
|
||||||
use http::{HeaderMap, HeaderValue, StatusCode};
|
use http::{HeaderMap, HeaderValue, StatusCode};
|
||||||
use hyper::Body;
|
use hyper::Body;
|
||||||
use kanidm_proto::constants::APPLICATION_JSON;
|
use kanidm_proto::constants::APPLICATION_JSON;
|
||||||
|
use kanidm_proto::internal::{ImageType, ImageValue};
|
||||||
use kanidm_proto::oauth2::{AuthorisationResponse, OidcDiscoveryResponse};
|
use kanidm_proto::oauth2::{AuthorisationResponse, OidcDiscoveryResponse};
|
||||||
use kanidm_proto::v1::Entry as ProtoEntry;
|
use kanidm_proto::v1::Entry as ProtoEntry;
|
||||||
use kanidmd_lib::idm::oauth2::{
|
use kanidmd_lib::idm::oauth2::{
|
||||||
|
@ -23,6 +24,7 @@ use kanidmd_lib::idm::oauth2::{
|
||||||
use kanidmd_lib::prelude::f_eq;
|
use kanidmd_lib::prelude::f_eq;
|
||||||
use kanidmd_lib::prelude::*;
|
use kanidmd_lib::prelude::*;
|
||||||
use kanidmd_lib::value::PartialValue;
|
use kanidmd_lib::value::PartialValue;
|
||||||
|
use kanidmd_lib::valueset::image::ImageValueThings;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
pub struct HTTPOauth2Error(Oauth2Error);
|
pub struct HTTPOauth2Error(Oauth2Error);
|
||||||
|
@ -104,6 +106,7 @@ pub async fn oauth2_public_post(
|
||||||
json_rest_event_post(state, classes, obj, kopid).await
|
json_rest_event_post(state, classes, obj, kopid).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a filter matching a given OAuth2 Resource Server
|
||||||
fn oauth2_id(rs_name: &str) -> Filter<FilterInvalid> {
|
fn oauth2_id(rs_name: &str) -> Filter<FilterInvalid> {
|
||||||
filter_all!(f_and!([
|
filter_all!(f_and!([
|
||||||
f_eq(Attribute::Class, EntryClass::OAuth2ResourceServer.into()),
|
f_eq(Attribute::Class, EntryClass::OAuth2ResourceServer.into()),
|
||||||
|
@ -222,6 +225,130 @@ pub async fn oauth2_id_delete(
|
||||||
to_axum_response(res)
|
to_axum_response(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// this returns the image for the user if the user has permissions
|
||||||
|
pub async fn oauth2_image_get(
|
||||||
|
State(state): State<ServerState>,
|
||||||
|
Extension(kopid): Extension<KOpId>,
|
||||||
|
Path(rs_name): Path<String>,
|
||||||
|
) -> Response<Body> {
|
||||||
|
let rs_filter = oauth2_id(&rs_name);
|
||||||
|
let res = state
|
||||||
|
.qe_r_ref
|
||||||
|
.handle_oauth2_rs_image_get_image(kopid.uat, rs_filter)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let image = match res {
|
||||||
|
Ok(image) => image,
|
||||||
|
Err(_err) => {
|
||||||
|
admin_error!(
|
||||||
|
"Unable to get image for oauth2 resource server: {}",
|
||||||
|
rs_name
|
||||||
|
);
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
|
return Response::builder()
|
||||||
|
.status(StatusCode::NOT_FOUND)
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#[allow(clippy::expect_used)]
|
||||||
|
Response::builder()
|
||||||
|
.header(CONTENT_TYPE, image.filetype.as_content_type_str())
|
||||||
|
.body(Body::from(image.contents))
|
||||||
|
.expect("Somehow failed to turn an image into a response!")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn oauth2_id_image_delete(
|
||||||
|
State(state): State<ServerState>,
|
||||||
|
Extension(kopid): Extension<KOpId>,
|
||||||
|
Path(rs_name): Path<String>,
|
||||||
|
) -> Response<Body> {
|
||||||
|
let rs_filter = oauth2_id(&rs_name);
|
||||||
|
let res = state
|
||||||
|
.qe_w_ref
|
||||||
|
.handle_oauth2_rs_image_delete(kopid.uat, rs_filter)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
to_axum_response(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn oauth2_id_image_post(
|
||||||
|
State(state): State<ServerState>,
|
||||||
|
Extension(kopid): Extension<KOpId>,
|
||||||
|
Path(rs_name): Path<String>,
|
||||||
|
mut multipart: axum::extract::Multipart,
|
||||||
|
) -> Response<Body> {
|
||||||
|
// because we might not get an image
|
||||||
|
let mut image: Option<ImageValue> = None;
|
||||||
|
|
||||||
|
while let Some(field) = multipart.next_field().await.unwrap_or(None) {
|
||||||
|
let filename = field.file_name().map(|f| f.to_string()).clone();
|
||||||
|
if let Some(filename) = filename {
|
||||||
|
let content_type = field.content_type().map(|f| f.to_string()).clone();
|
||||||
|
|
||||||
|
let content_type = match content_type {
|
||||||
|
Some(val) => {
|
||||||
|
if VALID_IMAGE_UPLOAD_CONTENT_TYPES.contains(&val.as_str()) {
|
||||||
|
val
|
||||||
|
} else {
|
||||||
|
debug!("Invalid content type: {}", val);
|
||||||
|
let res =
|
||||||
|
to_axum_response::<String>(Err(OperationError::InvalidRequestState));
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
debug!("No content type header provided");
|
||||||
|
let res = to_axum_response::<String>(Err(OperationError::InvalidRequestState));
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let data = match field.bytes().await {
|
||||||
|
Ok(val) => val,
|
||||||
|
Err(_e) => {
|
||||||
|
let res = to_axum_response::<String>(Err(OperationError::InvalidRequestState));
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let filetype = match ImageType::try_from_content_type(&content_type) {
|
||||||
|
Ok(val) => val,
|
||||||
|
Err(_err) => {
|
||||||
|
let res = to_axum_response::<String>(Err(OperationError::InvalidRequestState));
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
image = Some(ImageValue {
|
||||||
|
filetype,
|
||||||
|
filename: filename.to_string(),
|
||||||
|
contents: data.to_vec(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = match image {
|
||||||
|
Some(image) => {
|
||||||
|
let image_validation_result = image.validate_image();
|
||||||
|
if let Err(err) = image_validation_result {
|
||||||
|
admin_error!("Invalid image uploaded: {:?}", err);
|
||||||
|
return to_axum_response::<String>(Err(OperationError::InvalidRequestState));
|
||||||
|
}
|
||||||
|
|
||||||
|
let rs_name = oauth2_id(&rs_name);
|
||||||
|
state
|
||||||
|
.qe_w_ref
|
||||||
|
.handle_oauth2_rs_image_update(kopid.uat, rs_name, image)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
None => Err(OperationError::InvalidAttribute(
|
||||||
|
"No image included, did you mean to use the DELETE method?".to_string(),
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
to_axum_response(res)
|
||||||
|
}
|
||||||
|
|
||||||
// == OAUTH2 PROTOCOL FLOW HANDLERS ==
|
// == OAUTH2 PROTOCOL FLOW HANDLERS ==
|
||||||
//
|
//
|
||||||
// oauth2 (partial)
|
// oauth2 (partial)
|
||||||
|
|
|
@ -1447,6 +1447,10 @@ pub fn router(state: ServerState) -> Router<ServerState> {
|
||||||
.patch(super::oauth2::oauth2_id_patch)
|
.patch(super::oauth2::oauth2_id_patch)
|
||||||
.delete(super::oauth2::oauth2_id_delete),
|
.delete(super::oauth2::oauth2_id_delete),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/v1/oauth2/:rs_name/_image",
|
||||||
|
post(super::oauth2::oauth2_id_image_post).delete(super::oauth2::oauth2_id_image_delete),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/v1/oauth2/:rs_name/_basic_secret",
|
"/v1/oauth2/:rs_name/_basic_secret",
|
||||||
get(super::oauth2::oauth2_id_get_basic_secret),
|
get(super::oauth2::oauth2_id_get_basic_secret),
|
||||||
|
|
|
@ -767,7 +767,7 @@ pub async fn create_server_core(
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(
|
error!(
|
||||||
"Unable to configure INTERGATION TEST admin account -> {:?}",
|
"Unable to configure INTEGRATION TEST admin account -> {:?}",
|
||||||
e
|
e
|
||||||
);
|
);
|
||||||
return Err(());
|
return Err(());
|
||||||
|
@ -776,7 +776,7 @@ pub async fn create_server_core(
|
||||||
match idms_prox_write.commit() {
|
match idms_prox_write.commit() {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Unable to commit INTERGATION TEST setup -> {:?}", e);
|
error!("Unable to commit INTEGRATION TEST setup -> {:?}", e);
|
||||||
return Err(());
|
return Err(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,10 @@ path = "src/lib.rs"
|
||||||
name = "scaling_10k"
|
name = "scaling_10k"
|
||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "image_benches"
|
||||||
|
harness = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
base64 = { workspace = true }
|
base64 = { workspace = true }
|
||||||
base64urlsafedata = { workspace = true }
|
base64urlsafedata = { workspace = true }
|
||||||
|
@ -79,6 +83,14 @@ webauthn-rs = { workspace = true, features = [
|
||||||
webauthn-rs-core = { workspace = true }
|
webauthn-rs-core = { workspace = true }
|
||||||
zxcvbn = { workspace = true }
|
zxcvbn = { workspace = true }
|
||||||
serde_with = { workspace = true }
|
serde_with = { workspace = true }
|
||||||
|
hex.workspace = true
|
||||||
|
lodepng = { workspace = true }
|
||||||
|
image = { workspace = true, default-features = false, features = [
|
||||||
|
"gif",
|
||||||
|
"jpeg",
|
||||||
|
"webp",
|
||||||
|
] }
|
||||||
|
svg = { workspace = true }
|
||||||
|
|
||||||
# because windows really can't build without the bundled one
|
# because windows really can't build without the bundled one
|
||||||
[target.'cfg(target_family = "windows")'.dependencies]
|
[target.'cfg(target_family = "windows")'.dependencies]
|
||||||
|
|
138
server/lib/benches/image_benches.rs
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
/// This file contains benchmarks for the image module so we can work out the best order to run things in
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
|
use kanidmd_lib::valueset::image::jpg;
|
||||||
|
use kanidmd_lib::valueset::image::png;
|
||||||
|
|
||||||
|
pub fn bench_png_lodepng_validate(c: &mut Criterion) {
|
||||||
|
let mut group = c.benchmark_group("png_lodepng_validate");
|
||||||
|
group.bench_function("png_lodepng_validate_oversize", |b| {
|
||||||
|
let filename = black_box(format!(
|
||||||
|
"{}/src/valueset/image/test_images/oversize_dimensions.png",
|
||||||
|
env!("CARGO_MANIFEST_DIR")
|
||||||
|
));
|
||||||
|
let contents = black_box(std::fs::read(filename).unwrap());
|
||||||
|
b.iter(|| {
|
||||||
|
png::png_lodepng_validate(&contents, black_box(&"oversize_dimensions.png".to_string()))
|
||||||
|
})
|
||||||
|
});
|
||||||
|
group.bench_function("png_lodepng_validate_ok", |b| {
|
||||||
|
let filename = black_box(format!(
|
||||||
|
"{}/src/valueset/image/test_images/oversize_dimensions.png",
|
||||||
|
env!("CARGO_MANIFEST_DIR")
|
||||||
|
));
|
||||||
|
let contents = black_box(std::fs::read(filename).unwrap());
|
||||||
|
b.iter(|| {
|
||||||
|
png::png_lodepng_validate(&contents, black_box(&"oversize_dimensions.png".to_string()))
|
||||||
|
})
|
||||||
|
});
|
||||||
|
group.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bench_png_has_trailer(c: &mut Criterion) {
|
||||||
|
let mut group = c.benchmark_group("png_has_trailer");
|
||||||
|
group.bench_function("png_has_trailer_oversize", |b| {
|
||||||
|
let filename = black_box(format!(
|
||||||
|
"{}/src/valueset/image/test_images/oversize_dimensions.png",
|
||||||
|
env!("CARGO_MANIFEST_DIR")
|
||||||
|
));
|
||||||
|
let contents = black_box(std::fs::read(filename).unwrap());
|
||||||
|
b.iter(|| png::png_has_trailer(&contents));
|
||||||
|
});
|
||||||
|
group.bench_function("png_has_trailer_ok", |b| {
|
||||||
|
let filename = black_box(format!(
|
||||||
|
"{}/src/valueset/image/test_images/ok.png",
|
||||||
|
env!("CARGO_MANIFEST_DIR")
|
||||||
|
));
|
||||||
|
let contents = black_box(std::fs::read(filename).unwrap());
|
||||||
|
b.iter(|| png::png_has_trailer(&contents));
|
||||||
|
});
|
||||||
|
group.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bench_jpg(c: &mut Criterion) {
|
||||||
|
let mut group = c.benchmark_group("jpg");
|
||||||
|
group.bench_function("check_jpg_header", |b| {
|
||||||
|
let filename = black_box(format!(
|
||||||
|
"{}/src/valueset/image/test_images/oversize_dimensions.jpg",
|
||||||
|
env!("CARGO_MANIFEST_DIR")
|
||||||
|
));
|
||||||
|
let contents = black_box(std::fs::read(filename).unwrap());
|
||||||
|
b.iter(|| jpg::check_jpg_header(&contents));
|
||||||
|
});
|
||||||
|
group.bench_function("has_trailer", |b| {
|
||||||
|
let filename = black_box(format!(
|
||||||
|
"{}/src/valueset/image/test_images/oversize_dimensions.jpg",
|
||||||
|
env!("CARGO_MANIFEST_DIR")
|
||||||
|
));
|
||||||
|
let contents = black_box(std::fs::read(filename).unwrap());
|
||||||
|
b.iter(|| jpg::has_trailer(&contents));
|
||||||
|
});
|
||||||
|
group.bench_function("use_decoder", |b| {
|
||||||
|
let filename = black_box(format!(
|
||||||
|
"{}/src/valueset/image/test_images/oversize_dimensions.jpg",
|
||||||
|
env!("CARGO_MANIFEST_DIR")
|
||||||
|
));
|
||||||
|
let contents = black_box(std::fs::read(&filename).unwrap());
|
||||||
|
b.iter(|| jpg::validate_decoding(&filename, &contents, image::io::Limits::default()));
|
||||||
|
});
|
||||||
|
group.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compare_jpg(c: &mut Criterion) {
|
||||||
|
let mut group = c.benchmark_group("compare_jpg");
|
||||||
|
group.bench_function("header, trailer, decoder", |b| {
|
||||||
|
let filename = black_box(format!(
|
||||||
|
"{}/src/valueset/image/test_images/oversize_dimensions.jpg",
|
||||||
|
env!("CARGO_MANIFEST_DIR")
|
||||||
|
));
|
||||||
|
let contents = black_box(std::fs::read(&filename).unwrap());
|
||||||
|
b.iter(|| {
|
||||||
|
assert!(jpg::check_jpg_header(&contents).is_ok());
|
||||||
|
assert!(jpg::has_trailer(&contents).is_ok());
|
||||||
|
assert!(
|
||||||
|
jpg::validate_decoding(&filename, &contents, image::io::Limits::default()).is_ok()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
group.bench_function("trailer, header, decoder", |b| {
|
||||||
|
let filename = black_box(format!(
|
||||||
|
"{}/src/valueset/image/test_images/oversize_dimensions.jpg",
|
||||||
|
env!("CARGO_MANIFEST_DIR")
|
||||||
|
));
|
||||||
|
let contents = black_box(std::fs::read(&filename).unwrap());
|
||||||
|
b.iter(|| {
|
||||||
|
assert!(jpg::has_trailer(&contents).is_ok());
|
||||||
|
assert!(jpg::check_jpg_header(&contents).is_ok());
|
||||||
|
assert!(
|
||||||
|
jpg::validate_decoding(&filename, &contents, image::io::Limits::default()).is_ok()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
group.bench_function("decoder, trailer, header", |b| {
|
||||||
|
let filename = black_box(format!(
|
||||||
|
"{}/src/valueset/image/test_images/oversize_dimensions.jpg",
|
||||||
|
env!("CARGO_MANIFEST_DIR")
|
||||||
|
));
|
||||||
|
let contents = black_box(std::fs::read(&filename).unwrap());
|
||||||
|
b.iter(|| {
|
||||||
|
assert!(
|
||||||
|
jpg::validate_decoding(&filename, &contents, image::io::Limits::default()).is_ok()
|
||||||
|
);
|
||||||
|
assert!(jpg::has_trailer(&contents).is_ok());
|
||||||
|
assert!(jpg::check_jpg_header(&contents).is_ok());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
criterion_group!(
|
||||||
|
name = png_tests;
|
||||||
|
config = Criterion::default()
|
||||||
|
.measurement_time(Duration::from_secs(15))
|
||||||
|
.with_plots();
|
||||||
|
targets = bench_png_lodepng_validate, bench_png_has_trailer, bench_jpg, compare_jpg
|
||||||
|
);
|
||||||
|
criterion_main!(png_tests);
|
|
@ -2,6 +2,7 @@ use std::fmt;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use hashbrown::HashSet;
|
use hashbrown::HashSet;
|
||||||
|
use kanidm_proto::internal::ImageType;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_with::skip_serializing_none;
|
use serde_with::skip_serializing_none;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
@ -506,6 +507,16 @@ pub enum DbValueOauth2Session {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Internal representation of an image
|
||||||
|
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||||
|
pub enum DbValueImage {
|
||||||
|
V1 {
|
||||||
|
filename: String,
|
||||||
|
filetype: ImageType,
|
||||||
|
contents: Vec<u8>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub enum DbValueV1 {
|
pub enum DbValueV1 {
|
||||||
#[serde(rename = "U8")]
|
#[serde(rename = "U8")]
|
||||||
|
@ -651,6 +662,8 @@ pub enum DbValueSetV2 {
|
||||||
AuditLogString(Vec<(Cid, String)>),
|
AuditLogString(Vec<(Cid, String)>),
|
||||||
#[serde(rename = "EK")]
|
#[serde(rename = "EK")]
|
||||||
EcKeyPrivate(Vec<u8>),
|
EcKeyPrivate(Vec<u8>),
|
||||||
|
#[serde(rename = "IM")]
|
||||||
|
Image(Vec<DbValueImage>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DbValueSetV2 {
|
impl DbValueSetV2 {
|
||||||
|
@ -694,6 +707,7 @@ impl DbValueSetV2 {
|
||||||
DbValueSetV2::UiHint(set) => set.len(),
|
DbValueSetV2::UiHint(set) => set.len(),
|
||||||
DbValueSetV2::TotpSecret(set) => set.len(),
|
DbValueSetV2::TotpSecret(set) => set.len(),
|
||||||
DbValueSetV2::AuditLogString(set) => set.len(),
|
DbValueSetV2::AuditLogString(set) => set.len(),
|
||||||
|
DbValueSetV2::Image(set) => set.len(),
|
||||||
DbValueSetV2::EcKeyPrivate(_key) => 1, // here we have to hard code it because the Vec<u8>
|
DbValueSetV2::EcKeyPrivate(_key) => 1, // here we have to hard code it because the Vec<u8>
|
||||||
// represents the bytes of SINGLE(!) key
|
// represents the bytes of SINGLE(!) key
|
||||||
}
|
}
|
||||||
|
|
|
@ -1668,6 +1668,7 @@ lazy_static! {
|
||||||
Attribute::Rs256PrivateKeyDer,
|
Attribute::Rs256PrivateKeyDer,
|
||||||
Attribute::OAuth2JwtLegacyCryptoEnable,
|
Attribute::OAuth2JwtLegacyCryptoEnable,
|
||||||
Attribute::OAuth2PreferShortUsername,
|
Attribute::OAuth2PreferShortUsername,
|
||||||
|
Attribute::Image,
|
||||||
],
|
],
|
||||||
modify_removed_attrs: vec![
|
modify_removed_attrs: vec![
|
||||||
Attribute::Description,
|
Attribute::Description,
|
||||||
|
@ -1684,6 +1685,7 @@ lazy_static! {
|
||||||
Attribute::Rs256PrivateKeyDer,
|
Attribute::Rs256PrivateKeyDer,
|
||||||
Attribute::OAuth2JwtLegacyCryptoEnable,
|
Attribute::OAuth2JwtLegacyCryptoEnable,
|
||||||
Attribute::OAuth2PreferShortUsername,
|
Attribute::OAuth2PreferShortUsername,
|
||||||
|
Attribute::Image,
|
||||||
],
|
],
|
||||||
modify_present_attrs: vec![
|
modify_present_attrs: vec![
|
||||||
Attribute::Description,
|
Attribute::Description,
|
||||||
|
@ -1696,6 +1698,7 @@ lazy_static! {
|
||||||
Attribute::OAuth2AllowInsecureClientDisablePkce,
|
Attribute::OAuth2AllowInsecureClientDisablePkce,
|
||||||
Attribute::OAuth2JwtLegacyCryptoEnable,
|
Attribute::OAuth2JwtLegacyCryptoEnable,
|
||||||
Attribute::OAuth2PreferShortUsername,
|
Attribute::OAuth2PreferShortUsername,
|
||||||
|
Attribute::Image,
|
||||||
],
|
],
|
||||||
create_attrs: vec![
|
create_attrs: vec![
|
||||||
Attribute::Class,
|
Attribute::Class,
|
||||||
|
@ -1709,6 +1712,7 @@ lazy_static! {
|
||||||
Attribute::OAuth2AllowInsecureClientDisablePkce,
|
Attribute::OAuth2AllowInsecureClientDisablePkce,
|
||||||
Attribute::OAuth2JwtLegacyCryptoEnable,
|
Attribute::OAuth2JwtLegacyCryptoEnable,
|
||||||
Attribute::OAuth2PreferShortUsername,
|
Attribute::OAuth2PreferShortUsername,
|
||||||
|
Attribute::Image,
|
||||||
],
|
],
|
||||||
create_classes: vec![
|
create_classes: vec![
|
||||||
EntryClass::Object,
|
EntryClass::Object,
|
||||||
|
|
|
@ -88,6 +88,7 @@ pub enum Attribute {
|
||||||
GrantUiHint,
|
GrantUiHint,
|
||||||
Group,
|
Group,
|
||||||
IdVerificationEcKey,
|
IdVerificationEcKey,
|
||||||
|
Image,
|
||||||
Index,
|
Index,
|
||||||
IpaNtHash,
|
IpaNtHash,
|
||||||
IpaSshPubKey,
|
IpaSshPubKey,
|
||||||
|
@ -262,6 +263,7 @@ impl TryFrom<String> for Attribute {
|
||||||
ATTR_GRANT_UI_HINT => Attribute::GrantUiHint,
|
ATTR_GRANT_UI_HINT => Attribute::GrantUiHint,
|
||||||
ATTR_GROUP => Attribute::Group,
|
ATTR_GROUP => Attribute::Group,
|
||||||
ATTR_ID_VERIFICATION_ECKEY => Attribute::IdVerificationEcKey,
|
ATTR_ID_VERIFICATION_ECKEY => Attribute::IdVerificationEcKey,
|
||||||
|
ATTR_IMAGE => Attribute::Image,
|
||||||
ATTR_INDEX => Attribute::Index,
|
ATTR_INDEX => Attribute::Index,
|
||||||
ATTR_IPANTHASH => Attribute::IpaNtHash,
|
ATTR_IPANTHASH => Attribute::IpaNtHash,
|
||||||
ATTR_IPASSHPUBKEY => Attribute::IpaSshPubKey,
|
ATTR_IPASSHPUBKEY => Attribute::IpaSshPubKey,
|
||||||
|
@ -412,6 +414,7 @@ impl From<Attribute> for &'static str {
|
||||||
Attribute::GrantUiHint => ATTR_GRANT_UI_HINT,
|
Attribute::GrantUiHint => ATTR_GRANT_UI_HINT,
|
||||||
Attribute::Group => ATTR_GROUP,
|
Attribute::Group => ATTR_GROUP,
|
||||||
Attribute::IdVerificationEcKey => ATTR_ID_VERIFICATION_ECKEY,
|
Attribute::IdVerificationEcKey => ATTR_ID_VERIFICATION_ECKEY,
|
||||||
|
Attribute::Image => ATTR_IMAGE,
|
||||||
Attribute::Index => ATTR_INDEX,
|
Attribute::Index => ATTR_INDEX,
|
||||||
Attribute::IpaNtHash => ATTR_IPANTHASH,
|
Attribute::IpaNtHash => ATTR_IPANTHASH,
|
||||||
Attribute::IpaSshPubKey => ATTR_IPASSHPUBKEY,
|
Attribute::IpaSshPubKey => ATTR_IPASSHPUBKEY,
|
||||||
|
|
|
@ -751,6 +751,7 @@ pub static ref SCHEMA_CLASS_OAUTH2_RS: SchemaClass = SchemaClass {
|
||||||
Attribute::OAuth2JwtLegacyCryptoEnable.into(),
|
Attribute::OAuth2JwtLegacyCryptoEnable.into(),
|
||||||
Attribute::OAuth2PreferShortUsername.into(),
|
Attribute::OAuth2PreferShortUsername.into(),
|
||||||
Attribute::OAuth2RsOriginLanding.into(),
|
Attribute::OAuth2RsOriginLanding.into(),
|
||||||
|
Attribute::Image.into(),
|
||||||
],
|
],
|
||||||
systemmust: vec![
|
systemmust: vec![
|
||||||
Attribute::OAuth2RsName.into(),
|
Attribute::OAuth2RsName.into(),
|
||||||
|
|
|
@ -238,6 +238,8 @@ pub const UUID_SCHEMA_ATTR_AUTH_SESSION_EXPIRY: Uuid =
|
||||||
pub const UUID_SCHEMA_ATTR_AUTH_PRIVILEGE_EXPIRY: Uuid =
|
pub const UUID_SCHEMA_ATTR_AUTH_PRIVILEGE_EXPIRY: Uuid =
|
||||||
uuid!("00000000-0000-0000-0000-ffff00000142");
|
uuid!("00000000-0000-0000-0000-ffff00000142");
|
||||||
|
|
||||||
|
pub const UUID_SCHEMA_ATTR_IMAGE: Uuid = uuid!("00000000-0000-0000-0000-ffff00000143");
|
||||||
|
|
||||||
// System and domain infos
|
// System and domain infos
|
||||||
// I'd like to strongly criticise william of the past for making poor choices about these allocations.
|
// I'd like to strongly criticise william of the past for making poor choices about these allocations.
|
||||||
pub const UUID_SYSTEM_INFO: Uuid = uuid!("00000000-0000-0000-0000-ffffff000001");
|
pub const UUID_SYSTEM_INFO: Uuid = uuid!("00000000-0000-0000-0000-ffffff000001");
|
||||||
|
|
|
@ -30,7 +30,8 @@ use std::collections::{BTreeMap as Map, BTreeMap, BTreeSet};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use compact_jwt::JwsSigner;
|
use compact_jwt::JwsSigner;
|
||||||
use hashbrown::HashMap;
|
use hashbrown::{HashMap, HashSet};
|
||||||
|
use kanidm_proto::internal::ImageValue;
|
||||||
use kanidm_proto::v1::{
|
use kanidm_proto::v1::{
|
||||||
ConsistencyError, Entry as ProtoEntry, Filter as ProtoFilter, OperationError, SchemaError,
|
ConsistencyError, Entry as ProtoEntry, Filter as ProtoFilter, OperationError, SchemaError,
|
||||||
UiHint,
|
UiHint,
|
||||||
|
@ -2519,6 +2520,22 @@ impl<VALID, STATE> Entry<VALID, STATE> {
|
||||||
.and_then(|vs| vs.as_iutf8_set())
|
.and_then(|vs| vs.as_iutf8_set())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn get_ava_as_image(&self, attr: Attribute) -> Option<&HashSet<ImageValue>> {
|
||||||
|
self.attrs
|
||||||
|
.get(attr.as_ref())
|
||||||
|
.and_then(|vs| vs.as_imageset())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn get_ava_single_image(&self, attr: Attribute) -> Option<ImageValue> {
|
||||||
|
let images = self
|
||||||
|
.attrs
|
||||||
|
.get(attr.as_ref())
|
||||||
|
.and_then(|vs| vs.as_imageset())?;
|
||||||
|
images.iter().next().cloned()
|
||||||
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn get_ava_as_oauthscopes(&self, attr: Attribute) -> Option<impl Iterator<Item = &str>> {
|
pub fn get_ava_as_oauthscopes(&self, attr: Attribute) -> Option<impl Iterator<Item = &str>> {
|
||||||
self.attrs
|
self.attrs
|
||||||
|
|
|
@ -1135,7 +1135,7 @@ impl FilterResolved {
|
||||||
fn resolve_no_idx(fc: FilterComp, ev: &Identity) -> Option<Self> {
|
fn resolve_no_idx(fc: FilterComp, ev: &Identity) -> Option<Self> {
|
||||||
// ⚠️ ⚠️ ⚠️ ⚠️
|
// ⚠️ ⚠️ ⚠️ ⚠️
|
||||||
// Remember, this function means we have NO INDEX METADATA so we can only
|
// Remember, this function means we have NO INDEX METADATA so we can only
|
||||||
// asssign slopes to values we can GUARANTEE will EXIST.
|
// assign slopes to values we can GUARANTEE will EXIST.
|
||||||
match fc {
|
match fc {
|
||||||
FilterComp::Eq(a, v) => {
|
FilterComp::Eq(a, v) => {
|
||||||
// Since we have no index data, we manually configure a reasonable
|
// Since we have no index data, we manually configure a reasonable
|
||||||
|
|
|
@ -761,7 +761,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
// Store the intent id in the session (if needed) so that we can check the state at the
|
// Store the intent id in the session (if needed) so that we can check the state at the
|
||||||
// end of the update.
|
// end of the update.
|
||||||
|
|
||||||
// We need to pin the id from the intent token into the credential to ensure it's not re-used
|
// We need to pin the id from the intent token into the credential to ensure it's not reused
|
||||||
|
|
||||||
// Need to change this to the expiry time, so we can purge up to.
|
// Need to change this to the expiry time, so we can purge up to.
|
||||||
let session_id = uuid_from_duration(current_time + MAXIMUM_CRED_UPDATE_TTL, self.sid);
|
let session_id = uuid_from_duration(current_time + MAXIMUM_CRED_UPDATE_TTL, self.sid);
|
||||||
|
|
|
@ -19,6 +19,7 @@ use concread::cowcell::*;
|
||||||
use fernet::Fernet;
|
use fernet::Fernet;
|
||||||
use hashbrown::HashMap;
|
use hashbrown::HashMap;
|
||||||
use kanidm_proto::constants::*;
|
use kanidm_proto::constants::*;
|
||||||
|
|
||||||
pub use kanidm_proto::oauth2::{
|
pub use kanidm_proto::oauth2::{
|
||||||
AccessTokenIntrospectRequest, AccessTokenIntrospectResponse, AccessTokenRequest,
|
AccessTokenIntrospectRequest, AccessTokenIntrospectResponse, AccessTokenRequest,
|
||||||
AccessTokenResponse, AuthorisationRequest, CodeChallengeMethod, ErrorResponse, GrantTypeReq,
|
AccessTokenResponse, AuthorisationRequest, CodeChallengeMethod, ErrorResponse, GrantTypeReq,
|
||||||
|
@ -238,6 +239,8 @@ pub struct Oauth2RS {
|
||||||
scopes_supported: BTreeSet<String>,
|
scopes_supported: BTreeSet<String>,
|
||||||
prefer_short_username: bool,
|
prefer_short_username: bool,
|
||||||
type_: OauthRSType,
|
type_: OauthRSType,
|
||||||
|
/// Does the RS have a custom image set? If not, we use the default.
|
||||||
|
has_custom_image: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for Oauth2RS {
|
impl std::fmt::Debug for Oauth2RS {
|
||||||
|
@ -250,6 +253,7 @@ impl std::fmt::Debug for Oauth2RS {
|
||||||
.field("origin", &self.origin)
|
.field("origin", &self.origin)
|
||||||
.field("scope_maps", &self.scope_maps)
|
.field("scope_maps", &self.scope_maps)
|
||||||
.field("sup_scope_maps", &self.sup_scope_maps)
|
.field("sup_scope_maps", &self.sup_scope_maps)
|
||||||
|
.field("has_custom_image", &self.has_custom_image)
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -416,6 +420,8 @@ impl<'a> Oauth2ResourceServersWriteTransaction<'a> {
|
||||||
.get_ava_single_bool(Attribute::OAuth2PreferShortUsername)
|
.get_ava_single_bool(Attribute::OAuth2PreferShortUsername)
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
let has_custom_image = ent.get_ava_single_image(Attribute::Image).is_some();
|
||||||
|
|
||||||
let mut authorization_endpoint = self.inner.origin.clone();
|
let mut authorization_endpoint = self.inner.origin.clone();
|
||||||
authorization_endpoint.set_path("/ui/oauth2");
|
authorization_endpoint.set_path("/ui/oauth2");
|
||||||
|
|
||||||
|
@ -464,6 +470,7 @@ impl<'a> Oauth2ResourceServersWriteTransaction<'a> {
|
||||||
scopes_supported,
|
scopes_supported,
|
||||||
prefer_short_username,
|
prefer_short_username,
|
||||||
type_,
|
type_,
|
||||||
|
has_custom_image,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((client_id, rscfg))
|
Ok((client_id, rscfg))
|
||||||
|
@ -4741,7 +4748,7 @@ mod tests {
|
||||||
assert!(idms_prox_write.commit().is_ok());
|
assert!(idms_prox_write.commit().is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test that re-use of a refresh token is denied + terminates the session.
|
// Test that reuse of a refresh token is denied + terminates the session.
|
||||||
//
|
//
|
||||||
// https://www.ietf.org/archive/id/draft-ietf-oauth-security-topics-18.html#refresh_token_protection
|
// https://www.ietf.org/archive/id/draft-ietf-oauth-security-topics-18.html#refresh_token_protection
|
||||||
#[idm_test]
|
#[idm_test]
|
||||||
|
|
|
@ -642,7 +642,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
// Refuse to proceed if any entries are in the recycled or tombstone state, since subsequent
|
// Refuse to proceed if any entries are in the recycled or tombstone state, since subsequent
|
||||||
// operations WOULD fail.
|
// operations WOULD fail.
|
||||||
//
|
//
|
||||||
// I'm still a bit not sure what to do here though, because if we have uuid re-use from the
|
// I'm still a bit not sure what to do here though, because if we have uuid reuse from the
|
||||||
// external system, that would be a pain, but I think we have to do this. This would be an
|
// external system, that would be a pain, but I think we have to do this. This would be an
|
||||||
// exceedingly rare situation though since 389-ds doesn't allow external uuid to be set, nor
|
// exceedingly rare situation though since 389-ds doesn't allow external uuid to be set, nor
|
||||||
// does openldap. It would break both of their replication models for it to occur.
|
// does openldap. It would break both of their replication models for it to occur.
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use super::cid::Cid;
|
use super::cid::Cid;
|
||||||
use super::entry::EntryChangeState;
|
use super::entry::EntryChangeState;
|
||||||
use super::entry::State;
|
use super::entry::State;
|
||||||
|
use crate::be::dbvalue::DbValueImage;
|
||||||
use crate::entry::Eattrs;
|
use crate::entry::Eattrs;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::schema::{SchemaReadTransaction, SchemaTransaction};
|
use crate::schema::{SchemaReadTransaction, SchemaTransaction};
|
||||||
|
@ -400,6 +401,9 @@ pub enum ReplAttrV1 {
|
||||||
EcKeyPrivate {
|
EcKeyPrivate {
|
||||||
key: Vec<u8>,
|
key: Vec<u8>,
|
||||||
},
|
},
|
||||||
|
Image {
|
||||||
|
set: Vec<DbValueImage>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||||
|
|
|
@ -229,6 +229,7 @@ impl SchemaAttribute {
|
||||||
// Comparing on the label.
|
// Comparing on the label.
|
||||||
SyntaxType::TotpSecret => matches!(v, PartialValue::Utf8(_)),
|
SyntaxType::TotpSecret => matches!(v, PartialValue::Utf8(_)),
|
||||||
SyntaxType::AuditLogString => matches!(v, PartialValue::Utf8(_)),
|
SyntaxType::AuditLogString => matches!(v, PartialValue::Utf8(_)),
|
||||||
|
SyntaxType::Image => matches!(v, PartialValue::Utf8(_)),
|
||||||
};
|
};
|
||||||
if r {
|
if r {
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -280,6 +281,7 @@ impl SchemaAttribute {
|
||||||
SyntaxType::TotpSecret => matches!(v, Value::TotpSecret(_, _)),
|
SyntaxType::TotpSecret => matches!(v, Value::TotpSecret(_, _)),
|
||||||
SyntaxType::AuditLogString => matches!(v, Value::Utf8(_)),
|
SyntaxType::AuditLogString => matches!(v, Value::Utf8(_)),
|
||||||
SyntaxType::EcKeyPrivate => matches!(v, Value::EcKeyPrivate(_)),
|
SyntaxType::EcKeyPrivate => matches!(v, Value::EcKeyPrivate(_)),
|
||||||
|
SyntaxType::Image => matches!(v, Value::Image(_)),
|
||||||
};
|
};
|
||||||
if r {
|
if r {
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -374,7 +376,7 @@ impl From<SchemaAttribute> for EntryInitNew {
|
||||||
/// takes precedence. It is not possible to combine classes in an incompatible way due to these
|
/// takes precedence. It is not possible to combine classes in an incompatible way due to these
|
||||||
/// rules.
|
/// rules.
|
||||||
///
|
///
|
||||||
/// That in mind, and entry that has one of every possible class would probably be nonsensical,
|
/// That in mind, an entry that has one of every possible class would probably be nonsensical,
|
||||||
/// but the addition rules make it easy to construct and understand with concepts like [`access`]
|
/// but the addition rules make it easy to construct and understand with concepts like [`access`]
|
||||||
/// controls or accounts and posix extensions.
|
/// controls or accounts and posix extensions.
|
||||||
///
|
///
|
||||||
|
@ -1753,7 +1755,7 @@ impl<'a> SchemaWriteTransaction<'a> {
|
||||||
SchemaAttribute {
|
SchemaAttribute {
|
||||||
name: Attribute::UidNumber.into(),
|
name: Attribute::UidNumber.into(),
|
||||||
uuid: UUID_SCHEMA_ATTR_UIDNUMBER,
|
uuid: UUID_SCHEMA_ATTR_UIDNUMBER,
|
||||||
description: String::from("An LDAP Compatible uidNumber"),
|
description: String::from("An LDAP Compatible uidNumber."),
|
||||||
multivalue: false,
|
multivalue: false,
|
||||||
unique: false,
|
unique: false,
|
||||||
phantom: true,
|
phantom: true,
|
||||||
|
@ -1763,6 +1765,21 @@ impl<'a> SchemaWriteTransaction<'a> {
|
||||||
syntax: SyntaxType::Uint32,
|
syntax: SyntaxType::Uint32,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
self.attributes.insert(
|
||||||
|
Attribute::Image.into(),
|
||||||
|
SchemaAttribute {
|
||||||
|
name: Attribute::Image.into(),
|
||||||
|
uuid: UUID_SCHEMA_ATTR_IMAGE,
|
||||||
|
description: String::from("An image for display to end users."),
|
||||||
|
multivalue: false,
|
||||||
|
unique: false,
|
||||||
|
phantom: false,
|
||||||
|
sync_allowed: true,
|
||||||
|
replicated: true,
|
||||||
|
index: vec![],
|
||||||
|
syntax: SyntaxType::Image,
|
||||||
|
},
|
||||||
|
);
|
||||||
// end LDAP masking phantoms
|
// end LDAP masking phantoms
|
||||||
|
|
||||||
self.classes.insert(
|
self.classes.insert(
|
||||||
|
|
|
@ -18,7 +18,7 @@ pub(super) fn apply_search_access<'a>(
|
||||||
entry: &'a Arc<EntrySealedCommitted>,
|
entry: &'a Arc<EntrySealedCommitted>,
|
||||||
) -> SearchResult<'a> {
|
) -> SearchResult<'a> {
|
||||||
// This could be considered "slow" due to allocs each iter with the entry. We
|
// This could be considered "slow" due to allocs each iter with the entry. We
|
||||||
// could move these out of the loop and re-use, but there are likely risks to
|
// could move these out of the loop and reuse, but there are likely risks to
|
||||||
// that.
|
// that.
|
||||||
let mut denied = false;
|
let mut denied = false;
|
||||||
let mut grant = false;
|
let mut grant = false;
|
||||||
|
@ -144,12 +144,13 @@ fn search_oauth2_filter_entry<'a>(
|
||||||
security_access!(entry = ?entry.get_uuid(), ident = ?iuser.entry.get_uuid2rdn(), "ident is a memberof a group granted an oauth2 scope by this entry");
|
security_access!(entry = ?entry.get_uuid(), ident = ?iuser.entry.get_uuid2rdn(), "ident is a memberof a group granted an oauth2 scope by this entry");
|
||||||
|
|
||||||
return AccessResult::Allow(btreeset!(
|
return AccessResult::Allow(btreeset!(
|
||||||
ATTR_CLASS,
|
Attribute::Class.as_ref(),
|
||||||
ATTR_DISPLAYNAME,
|
Attribute::DisplayName.as_ref(),
|
||||||
ATTR_UUID,
|
Attribute::Uuid.as_ref(),
|
||||||
ATTR_OAUTH2_RS_NAME,
|
Attribute::OAuth2RsName.as_ref(),
|
||||||
ATTR_OAUTH2_RS_ORIGIN,
|
Attribute::OAuth2RsOrigin.as_ref(),
|
||||||
ATTR_OAUTH2_RS_ORIGIN_LANDING
|
Attribute::OAuth2RsOriginLanding.as_ref(),
|
||||||
|
Attribute::Image.as_ref()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
AccessResult::Ignore
|
AccessResult::Ignore
|
||||||
|
|
|
@ -540,6 +540,8 @@ pub trait QueryServerTransaction<'a> {
|
||||||
}
|
}
|
||||||
SyntaxType::JsonFilter => Value::new_json_filter_s(value)
|
SyntaxType::JsonFilter => Value::new_json_filter_s(value)
|
||||||
.ok_or_else(|| OperationError::InvalidAttribute("Invalid Filter syntax".to_string())),
|
.ok_or_else(|| OperationError::InvalidAttribute("Invalid Filter syntax".to_string())),
|
||||||
|
SyntaxType::Image => Value::new_image(value),
|
||||||
|
|
||||||
SyntaxType::Credential => Err(OperationError::InvalidAttribute("Credentials can not be supplied through modification - please use the IDM api".to_string())),
|
SyntaxType::Credential => Err(OperationError::InvalidAttribute("Credentials can not be supplied through modification - please use the IDM api".to_string())),
|
||||||
SyntaxType::SecretUtf8String => Err(OperationError::InvalidAttribute("Radius secrets can not be supplied through modification - please use the IDM api".to_string())),
|
SyntaxType::SecretUtf8String => Err(OperationError::InvalidAttribute("Radius secrets can not be supplied through modification - please use the IDM api".to_string())),
|
||||||
SyntaxType::SshKey => Err(OperationError::InvalidAttribute("SSH public keys can not be supplied through modification - please use the IDM api".to_string())),
|
SyntaxType::SshKey => Err(OperationError::InvalidAttribute("SSH public keys can not be supplied through modification - please use the IDM api".to_string())),
|
||||||
|
@ -681,6 +683,7 @@ pub trait QueryServerTransaction<'a> {
|
||||||
}),
|
}),
|
||||||
SyntaxType::AuditLogString => Ok(PartialValue::new_utf8s(value)),
|
SyntaxType::AuditLogString => Ok(PartialValue::new_utf8s(value)),
|
||||||
SyntaxType::EcKeyPrivate => Ok(PartialValue::SecretValue),
|
SyntaxType::EcKeyPrivate => Ok(PartialValue::SecretValue),
|
||||||
|
SyntaxType::Image => Ok(PartialValue::new_utf8s(value)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
|
|
@ -63,7 +63,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
||||||
if pre_candidates.is_empty() {
|
if pre_candidates.is_empty() {
|
||||||
if me.ident.is_internal() {
|
if me.ident.is_internal() {
|
||||||
trace!(
|
trace!(
|
||||||
"modify: no candidates match filter ... continuing {:?}",
|
"modify_pre_apply: no candidates match filter ... continuing {:?}",
|
||||||
me.filter
|
me.filter
|
||||||
);
|
);
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
|
@ -76,8 +76,8 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
trace!("modify: pre_candidates -> {:?}", pre_candidates);
|
trace!("modify_pre_apply: pre_candidates -> {:?}", pre_candidates);
|
||||||
trace!("modify: modlist -> {:?}", me.modlist);
|
trace!("modify_pre_apply: modlist -> {:?}", me.modlist);
|
||||||
|
|
||||||
// Are we allowed to make the changes we want to?
|
// Are we allowed to make the changes we want to?
|
||||||
// modify_allow_operation
|
// modify_allow_operation
|
||||||
|
|
|
@ -17,6 +17,7 @@ use std::time::Duration;
|
||||||
use base64::{engine::general_purpose, Engine as _};
|
use base64::{engine::general_purpose, Engine as _};
|
||||||
use compact_jwt::JwsSigner;
|
use compact_jwt::JwsSigner;
|
||||||
use hashbrown::HashSet;
|
use hashbrown::HashSet;
|
||||||
|
use kanidm_proto::internal::ImageValue;
|
||||||
use num_enum::TryFromPrimitive;
|
use num_enum::TryFromPrimitive;
|
||||||
use openssl::ec::EcKey;
|
use openssl::ec::EcKey;
|
||||||
use openssl::pkey::Private;
|
use openssl::pkey::Private;
|
||||||
|
@ -33,6 +34,7 @@ use crate::credential::{totp::Totp, Credential};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::repl::cid::Cid;
|
use crate::repl::cid::Cid;
|
||||||
use crate::server::identity::IdentityId;
|
use crate::server::identity::IdentityId;
|
||||||
|
use crate::valueset::image::ImageValueThings;
|
||||||
use crate::valueset::uuid_to_proto_string;
|
use crate::valueset::uuid_to_proto_string;
|
||||||
use kanidm_proto::v1::ApiTokenPurpose;
|
use kanidm_proto::v1::ApiTokenPurpose;
|
||||||
use kanidm_proto::v1::Filter as ProtoFilter;
|
use kanidm_proto::v1::Filter as ProtoFilter;
|
||||||
|
@ -254,6 +256,7 @@ pub enum SyntaxType {
|
||||||
ApiToken = 31,
|
ApiToken = 31,
|
||||||
AuditLogString = 32,
|
AuditLogString = 32,
|
||||||
EcKeyPrivate = 33,
|
EcKeyPrivate = 33,
|
||||||
|
Image = 34,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&str> for SyntaxType {
|
impl TryFrom<&str> for SyntaxType {
|
||||||
|
@ -339,6 +342,7 @@ impl fmt::Display for SyntaxType {
|
||||||
SyntaxType::ApiToken => "APITOKEN",
|
SyntaxType::ApiToken => "APITOKEN",
|
||||||
SyntaxType::AuditLogString => "AUDIT_LOG_STRING",
|
SyntaxType::AuditLogString => "AUDIT_LOG_STRING",
|
||||||
SyntaxType::EcKeyPrivate => "EC_KEY_PRIVATE",
|
SyntaxType::EcKeyPrivate => "EC_KEY_PRIVATE",
|
||||||
|
SyntaxType::Image => "IMAGE",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -347,7 +351,7 @@ impl fmt::Display for SyntaxType {
|
||||||
/// against a complete Value within a set in an Entry.
|
/// against a complete Value within a set in an Entry.
|
||||||
///
|
///
|
||||||
/// A partialValue is typically used when you need to match against a value, but without
|
/// A partialValue is typically used when you need to match against a value, but without
|
||||||
/// requiring all of it's data or expression. This is common in Filters or other direct
|
/// requiring all of its data or expression. This is common in Filters or other direct
|
||||||
/// lookups and requests.
|
/// lookups and requests.
|
||||||
#[derive(Hash, Debug, Clone, Eq, Ord, PartialOrd, PartialEq, Deserialize, Serialize)]
|
#[derive(Hash, Debug, Clone, Eq, Ord, PartialOrd, PartialEq, Deserialize, Serialize)]
|
||||||
pub enum PartialValue {
|
pub enum PartialValue {
|
||||||
|
@ -387,6 +391,8 @@ pub enum PartialValue {
|
||||||
UiHint(UiHint),
|
UiHint(UiHint),
|
||||||
Passkey(Uuid),
|
Passkey(Uuid),
|
||||||
DeviceKey(Uuid),
|
DeviceKey(Uuid),
|
||||||
|
/// We compare on the value hash
|
||||||
|
Image(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<SyntaxType> for PartialValue {
|
impl From<SyntaxType> for PartialValue {
|
||||||
|
@ -702,6 +708,10 @@ impl PartialValue {
|
||||||
Uuid::parse_str(us).map(PartialValue::DeviceKey).ok()
|
Uuid::parse_str(us).map(PartialValue::DeviceKey).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new_image(input: &str) -> Self {
|
||||||
|
PartialValue::Image(input.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn to_str(&self) -> Option<&str> {
|
pub fn to_str(&self) -> Option<&str> {
|
||||||
match self {
|
match self {
|
||||||
PartialValue::Utf8(s) => Some(s.as_str()),
|
PartialValue::Utf8(s) => Some(s.as_str()),
|
||||||
|
@ -759,6 +769,7 @@ impl PartialValue {
|
||||||
PartialValue::PhoneNumber(a) => a.to_string(),
|
PartialValue::PhoneNumber(a) => a.to_string(),
|
||||||
PartialValue::IntentToken(u) => u.clone(),
|
PartialValue::IntentToken(u) => u.clone(),
|
||||||
PartialValue::UiHint(u) => (*u as u16).to_string(),
|
PartialValue::UiHint(u) => (*u as u16).to_string(),
|
||||||
|
PartialValue::Image(imagehash) => imagehash.to_owned(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -910,9 +921,9 @@ pub struct Oauth2Session {
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Value {
|
pub enum Value {
|
||||||
Utf8(String),
|
Utf8(String),
|
||||||
// Case insensitive string
|
/// Case insensitive string
|
||||||
Iutf8(String),
|
Iutf8(String),
|
||||||
/// Case insensitive Name for a thing?
|
/// Case insensitive Name for a thing
|
||||||
Iname(String),
|
Iname(String),
|
||||||
Uuid(Uuid),
|
Uuid(Uuid),
|
||||||
Bool(bool),
|
Bool(bool),
|
||||||
|
@ -936,8 +947,6 @@ pub enum Value {
|
||||||
OauthScopeMap(Uuid, BTreeSet<String>),
|
OauthScopeMap(Uuid, BTreeSet<String>),
|
||||||
PrivateBinary(Vec<u8>),
|
PrivateBinary(Vec<u8>),
|
||||||
PublicBinary(String, Vec<u8>),
|
PublicBinary(String, Vec<u8>),
|
||||||
// Enumeration(String),
|
|
||||||
// Float64(f64),
|
|
||||||
RestrictedString(String),
|
RestrictedString(String),
|
||||||
IntentToken(String, IntentTokenState),
|
IntentToken(String, IntentTokenState),
|
||||||
Passkey(Uuid, String, PasskeyV4),
|
Passkey(Uuid, String, PasskeyV4),
|
||||||
|
@ -954,6 +963,8 @@ pub enum Value {
|
||||||
TotpSecret(String, Totp),
|
TotpSecret(String, Totp),
|
||||||
AuditLogString(Cid, String),
|
AuditLogString(Cid, String),
|
||||||
EcKeyPrivate(EcKey<Private>),
|
EcKeyPrivate(EcKey<Private>),
|
||||||
|
|
||||||
|
Image(ImageValue),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for Value {
|
impl PartialEq for Value {
|
||||||
|
@ -992,6 +1003,9 @@ impl PartialEq for Value {
|
||||||
// OauthScopeMap
|
// OauthScopeMap
|
||||||
(Value::OauthScopeMap(a, c), Value::OauthScopeMap(b, d)) => a.eq(b) && c.eq(d),
|
(Value::OauthScopeMap(a, c), Value::OauthScopeMap(b, d)) => a.eq(b) && c.eq(d),
|
||||||
|
|
||||||
|
(Value::Image(image1), Value::Image(image2)) => {
|
||||||
|
image1.hash_imagevalue().eq(&image2.hash_imagevalue())
|
||||||
|
}
|
||||||
(Value::Address(_), Value::Address(_))
|
(Value::Address(_), Value::Address(_))
|
||||||
| (Value::PrivateBinary(_), Value::PrivateBinary(_))
|
| (Value::PrivateBinary(_), Value::PrivateBinary(_))
|
||||||
| (Value::SecretValue(_), Value::SecretValue(_)) => false,
|
| (Value::SecretValue(_), Value::SecretValue(_)) => false,
|
||||||
|
@ -1233,6 +1247,13 @@ impl Value {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Want a `Value::Image`? use this!
|
||||||
|
pub fn new_image(input: &str) -> Result<Self, OperationError> {
|
||||||
|
serde_json::from_str::<ImageValue>(input)
|
||||||
|
.map(Value::Image)
|
||||||
|
.map_err(|_e| OperationError::InvalidValueState)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn new_secret_str(cleartext: &str) -> Self {
|
pub fn new_secret_str(cleartext: &str) -> Self {
|
||||||
Value::SecretValue(cleartext.to_string())
|
Value::SecretValue(cleartext.to_string())
|
||||||
}
|
}
|
||||||
|
@ -1710,7 +1731,7 @@ impl Value {
|
||||||
&& Value::validate_singleline(a)
|
&& Value::validate_singleline(a)
|
||||||
&& Value::validate_singleline(b)
|
&& Value::validate_singleline(b)
|
||||||
}
|
}
|
||||||
|
Value::Image(image) => image.validate_image().is_ok(),
|
||||||
Value::Iname(s) => {
|
Value::Iname(s) => {
|
||||||
Value::validate_str_escapes(s)
|
Value::validate_str_escapes(s)
|
||||||
&& Value::validate_iname(s)
|
&& Value::validate_iname(s)
|
||||||
|
|
|
@ -331,7 +331,7 @@ impl ValueSetT for ValueSetEmailAddress {
|
||||||
let r = self.set.remove(a);
|
let r = self.set.remove(a);
|
||||||
if &self.primary == a {
|
if &self.primary == a {
|
||||||
// if we can, inject another former address into primary.
|
// if we can, inject another former address into primary.
|
||||||
if let Some(n) = self.set.iter().next().cloned() {
|
if let Some(n) = self.set.iter().take(1).next().cloned() {
|
||||||
self.primary = n
|
self.primary = n
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
126
server/lib/src/valueset/image/jpg.rs
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
use image::codecs::jpeg::JpegDecoder;
|
||||||
|
use image::ImageDecoder;
|
||||||
|
use sketching::*;
|
||||||
|
|
||||||
|
use super::ImageValidationError;
|
||||||
|
|
||||||
|
const JPEG_MAGIC: [u8; 2] = [0xff, 0xd8];
|
||||||
|
const EOI_MAGIC: [u8; 2] = [0xff, 0xd9];
|
||||||
|
const SOS_MARKER: [u8; 2] = [0xff, 0xda];
|
||||||
|
|
||||||
|
/// Checks to see if it has a valid JPEG magic bytes header
|
||||||
|
pub fn check_jpg_header(contents: &[u8]) -> Result<(), ImageValidationError> {
|
||||||
|
if !contents.starts_with(&JPEG_MAGIC) {
|
||||||
|
return Err(ImageValidationError::InvalidImage(
|
||||||
|
"Failed to parse JPEG file, invalid magic bytes".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// It's public so we can use it in benchmarking
|
||||||
|
/// Check to see if JPG is affected by acropalypse issues, returns `Ok(true)` if it is
|
||||||
|
/// based on <https://github.com/lordofpipes/acropadetect/blob/main/src/detect.ts>
|
||||||
|
pub fn has_trailer(contents: &Vec<u8>) -> Result<bool, ImageValidationError> {
|
||||||
|
let buf = contents.as_slice();
|
||||||
|
|
||||||
|
let mut pos = JPEG_MAGIC.len();
|
||||||
|
|
||||||
|
while pos < buf.len() {
|
||||||
|
let marker = &buf[pos..pos + 2];
|
||||||
|
pos += 2;
|
||||||
|
|
||||||
|
let segment_size_bytes: &[u8] = &buf[pos..pos + 2];
|
||||||
|
let segment_size = u16::from_be_bytes(segment_size_bytes.try_into().map_err(|_| {
|
||||||
|
ImageValidationError::InvalidImage("JPEG segment size bytes were invalid!".to_string())
|
||||||
|
})?);
|
||||||
|
// we do not add 2 because the size prefix includes the size of the size prefix
|
||||||
|
pos += segment_size as usize;
|
||||||
|
|
||||||
|
if marker == SOS_MARKER {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setting this to a big value so we can see if we don't find the EOI marker
|
||||||
|
let mut eoi_index = buf.len() * 2;
|
||||||
|
trace!("buffer length: {}", buf.len());
|
||||||
|
|
||||||
|
// iterate through the file looking for the EOI_MAGIC bytes
|
||||||
|
for i in pos..=(buf.len() - EOI_MAGIC.len()) {
|
||||||
|
if buf[i..(i + EOI_MAGIC.len())] == EOI_MAGIC {
|
||||||
|
eoi_index = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if eoi_index > buf.len() {
|
||||||
|
Err(ImageValidationError::InvalidImage(
|
||||||
|
"End of image magic bytes not found in JPEG".to_string(),
|
||||||
|
))
|
||||||
|
} else if (eoi_index + 2) < buf.len() {
|
||||||
|
// there's still bytes in the buffer after the EOI magic bytes
|
||||||
|
#[cfg(any(test, debug_assertions))]
|
||||||
|
println!(
|
||||||
|
"we're at pos: {} and buf len is {}, is not OK",
|
||||||
|
eoi_index,
|
||||||
|
buf.len()
|
||||||
|
);
|
||||||
|
Ok(true)
|
||||||
|
} else {
|
||||||
|
#[cfg(any(test, debug_assertions))]
|
||||||
|
println!(
|
||||||
|
"we're at pos: {} and buf len is {}, is OK",
|
||||||
|
eoi_index,
|
||||||
|
buf.len()
|
||||||
|
);
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_decoding(
|
||||||
|
filename: &str,
|
||||||
|
contents: &[u8],
|
||||||
|
limits: image::io::Limits,
|
||||||
|
) -> Result<(), ImageValidationError> {
|
||||||
|
let mut decoder = match JpegDecoder::new(contents) {
|
||||||
|
Ok(val) => val,
|
||||||
|
Err(err) => {
|
||||||
|
return Err(ImageValidationError::InvalidImage(format!(
|
||||||
|
"Failed to parse {} as JPG: {:?}",
|
||||||
|
filename, err
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match decoder.set_limits(limits) {
|
||||||
|
Err(err) => {
|
||||||
|
sketching::admin_warn!(
|
||||||
|
"Image validation failed while validating {}: {:?}",
|
||||||
|
filename,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
Err(ImageValidationError::ExceedsMaxDimensions)
|
||||||
|
}
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_jpg_has_trailer() {
|
||||||
|
let file_contents = std::fs::read(format!(
|
||||||
|
"{}/src/valueset/image/test_images/oversize_dimensions.jpg",
|
||||||
|
env!("CARGO_MANIFEST_DIR")
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
assert!(!has_trailer(&file_contents).unwrap());
|
||||||
|
|
||||||
|
// checking a known bad imagee
|
||||||
|
let file_contents = std::fs::read(format!(
|
||||||
|
"{}/src/valueset/image/test_images/windows11_3_cropped.jpg",
|
||||||
|
env!("CARGO_MANIFEST_DIR")
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
// let test_bytes = vec![0xff, 0xd8, 0xff, 0xda, 0xff, 0xd9];
|
||||||
|
assert!(has_trailer(&file_contents).unwrap());
|
||||||
|
}
|
534
server/lib/src/valueset/image/mod.rs
Normal file
|
@ -0,0 +1,534 @@
|
||||||
|
#![allow(dead_code)]
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use hashbrown::HashSet;
|
||||||
|
use image::codecs::gif::GifDecoder;
|
||||||
|
use image::codecs::webp::WebPDecoder;
|
||||||
|
use image::ImageDecoder;
|
||||||
|
use kanidm_proto::internal::{ImageType, ImageValue};
|
||||||
|
|
||||||
|
use crate::be::dbvalue::DbValueImage;
|
||||||
|
use crate::prelude::*;
|
||||||
|
use crate::repl::proto::ReplAttrV1;
|
||||||
|
use crate::schema::SchemaAttribute;
|
||||||
|
use crate::valueset::{DbValueSetV2, ValueSet};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ValueSetImage {
|
||||||
|
set: HashSet<ImageValue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) const MAX_IMAGE_HEIGHT: u32 = 1024;
|
||||||
|
pub(crate) const MAX_IMAGE_WIDTH: u32 = 1024;
|
||||||
|
/// 128kb should be enough for anyone... right? :D
|
||||||
|
pub(crate) const MAX_FILE_SIZE: u32 = 1024 * 128;
|
||||||
|
|
||||||
|
const WEBP_MAGIC: &[u8; 4] = b"RIFF";
|
||||||
|
|
||||||
|
pub mod jpg;
|
||||||
|
pub mod png;
|
||||||
|
|
||||||
|
pub trait ImageValueThings {
|
||||||
|
fn validate_image(&self) -> Result<(), ImageValidationError>;
|
||||||
|
fn validate_is_png(&self) -> Result<(), ImageValidationError>;
|
||||||
|
fn validate_is_gif(&self) -> Result<(), ImageValidationError>;
|
||||||
|
fn validate_is_jpg(&self) -> Result<(), ImageValidationError>;
|
||||||
|
fn validate_is_webp(&self) -> Result<(), ImageValidationError>;
|
||||||
|
fn validate_is_svg(&self) -> Result<(), ImageValidationError>;
|
||||||
|
|
||||||
|
/// A sha256 of the filename/type/contents
|
||||||
|
fn hash_imagevalue(&self) -> String;
|
||||||
|
|
||||||
|
fn get_limits(&self) -> image::io::Limits {
|
||||||
|
let mut limits = image::io::Limits::default();
|
||||||
|
limits.max_image_height = Some(MAX_IMAGE_HEIGHT);
|
||||||
|
limits.max_image_width = Some(MAX_IMAGE_WIDTH);
|
||||||
|
limits
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ImageValidationError {
|
||||||
|
Acropalypse(String),
|
||||||
|
ExceedsMaxWidth,
|
||||||
|
ExceedsMaxHeight,
|
||||||
|
ExceedsMaxDimensions,
|
||||||
|
ExceedsMaxFileSize,
|
||||||
|
InvalidImage(String),
|
||||||
|
InvalidPngPrelude,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ImageValidationError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
ImageValidationError::ExceedsMaxWidth => f.write_fmt(format_args!(
|
||||||
|
"Exceeds the maximum width: {}",
|
||||||
|
MAX_IMAGE_WIDTH
|
||||||
|
)),
|
||||||
|
ImageValidationError::ExceedsMaxHeight => f.write_fmt(format_args!(
|
||||||
|
"Exceeds the maximum height: {}",
|
||||||
|
MAX_IMAGE_HEIGHT
|
||||||
|
)),
|
||||||
|
ImageValidationError::ExceedsMaxFileSize => f.write_fmt(format_args!(
|
||||||
|
"Exceeds maximum file size of {}",
|
||||||
|
MAX_FILE_SIZE
|
||||||
|
)),
|
||||||
|
ImageValidationError::InvalidImage(message) => {
|
||||||
|
if !message.is_empty() {
|
||||||
|
f.write_fmt(format_args!("Invalid Image: {}", message))
|
||||||
|
} else {
|
||||||
|
f.write_str("Invalid Image")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImageValidationError::ExceedsMaxDimensions => f.write_fmt(format_args!(
|
||||||
|
"Image exceeds max dimensions of {}x{}",
|
||||||
|
MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT
|
||||||
|
)),
|
||||||
|
ImageValidationError::Acropalypse(message) => {
|
||||||
|
if !message.is_empty() {
|
||||||
|
f.write_fmt(format_args!(
|
||||||
|
"Image has extra data, is vulnerable to Acropalypse: {}",
|
||||||
|
message
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
f.write_str("Image has extra data, is vulnerable to Acropalypse")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImageValidationError::InvalidPngPrelude => {
|
||||||
|
f.write_str("Image has an invalid PNG prelude and is likely corrupt.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImageValueThings for ImageValue {
|
||||||
|
fn validate_image(&self) -> Result<(), ImageValidationError> {
|
||||||
|
if self.contents.len() > MAX_FILE_SIZE as usize {
|
||||||
|
return Err(ImageValidationError::ExceedsMaxFileSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.filetype {
|
||||||
|
ImageType::Gif => self.validate_is_gif(),
|
||||||
|
ImageType::Png => self.validate_is_png(),
|
||||||
|
ImageType::Svg => self.validate_is_svg(),
|
||||||
|
ImageType::Jpg => self.validate_is_jpg(),
|
||||||
|
ImageType::Webp => self.validate_is_webp(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate the PNG file contents, and that it's actually a PNG
|
||||||
|
fn validate_is_png(&self) -> Result<(), ImageValidationError> {
|
||||||
|
// based on code here: https://blog.cloudflare.com/how-cloudflare-images-addressed-the-acropalypse-vulnerability/
|
||||||
|
|
||||||
|
// this takes µs to run, where lodepng takes ms, so it comes first
|
||||||
|
if png::png_has_trailer(&self.contents)? {
|
||||||
|
return Err(ImageValidationError::Acropalypse(
|
||||||
|
"PNG file has a trailer which likely indicates the acropalypse vulnerability!"
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
png::png_lodepng_validate(&self.contents, &self.filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// validate the JPG file contents, and that it's actually a JPG
|
||||||
|
fn validate_is_jpg(&self) -> Result<(), ImageValidationError> {
|
||||||
|
// check it starts with a valid header
|
||||||
|
jpg::check_jpg_header(&self.contents)?;
|
||||||
|
|
||||||
|
jpg::validate_decoding(&self.filename, &self.contents, self.get_limits())?;
|
||||||
|
|
||||||
|
if jpg::has_trailer(&self.contents)? {
|
||||||
|
Err(ImageValidationError::Acropalypse(
|
||||||
|
"File has a trailer which likely indicates the acropalypse vulnerability!"
|
||||||
|
.to_string(),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// validate the GIF file contents, and that it's actually a GIF
|
||||||
|
fn validate_is_gif(&self) -> Result<(), ImageValidationError> {
|
||||||
|
let Ok(mut decoder) = GifDecoder::new(&self.contents[..]) else {
|
||||||
|
return Err(ImageValidationError::InvalidImage(
|
||||||
|
"Failed to parse GIF".to_string(),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
let limit_result = decoder.set_limits(self.get_limits());
|
||||||
|
if limit_result.is_err() {
|
||||||
|
Err(ImageValidationError::ExceedsMaxDimensions)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// validate the SVG file contents, and that it's actually a SVG (ish)
|
||||||
|
fn validate_is_svg(&self) -> Result<(), ImageValidationError> {
|
||||||
|
// svg is a string so let's do this
|
||||||
|
let svg_string = std::str::from_utf8(&self.contents).map_err(|e| {
|
||||||
|
ImageValidationError::InvalidImage(format!(
|
||||||
|
"Failed to parse SVG {} as a unicode string: {:?}",
|
||||||
|
self.hash_imagevalue(),
|
||||||
|
e
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
svg::read(svg_string).map_err(|e| {
|
||||||
|
ImageValidationError::InvalidImage(format!(
|
||||||
|
"Failed to parse {} as SVG: {:?}",
|
||||||
|
self.hash_imagevalue(),
|
||||||
|
e
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// validate the WebP file contents, and that it's actually a WebP file (as far as we can tell)
|
||||||
|
fn validate_is_webp(&self) -> Result<(), ImageValidationError> {
|
||||||
|
if !self.contents.starts_with(WEBP_MAGIC) {
|
||||||
|
return Err(ImageValidationError::InvalidImage(
|
||||||
|
"Failed to parse WebP file, invalid magic bytes".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let Ok(mut decoder) = WebPDecoder::new(&self.contents[..]) else {
|
||||||
|
return Err(ImageValidationError::InvalidImage(
|
||||||
|
"Failed to parse WebP file".to_string(),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
match decoder.set_limits(self.get_limits()) {
|
||||||
|
Err(err) => {
|
||||||
|
sketching::admin_warn!(
|
||||||
|
"Image validation failed while validating {}: {:?}",
|
||||||
|
self.filename,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
Err(ImageValidationError::ExceedsMaxDimensions)
|
||||||
|
}
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A sha256 of the filename/type/contents, uses openssl so has to live here
|
||||||
|
/// because proto don't need that jazz
|
||||||
|
fn hash_imagevalue(&self) -> String {
|
||||||
|
let filetype_repr = [self.filetype.clone() as u8];
|
||||||
|
let mut hasher = openssl::sha::Sha256::new();
|
||||||
|
hasher.update(self.filename.as_bytes());
|
||||||
|
hasher.update(&filetype_repr);
|
||||||
|
hasher.update(&self.contents);
|
||||||
|
hex::encode(hasher.finish())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValueSetImage {
|
||||||
|
pub fn new(image: ImageValue) -> Box<Self> {
|
||||||
|
let mut set = HashSet::new();
|
||||||
|
match image.validate_image() {
|
||||||
|
Ok(_) => {
|
||||||
|
set.insert(image);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
admin_error!(
|
||||||
|
"Image {} didn't pass validation, not adding to value! Error: {:?}",
|
||||||
|
image.filename,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Box::new(ValueSetImage { set })
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the image, return a bool if there was a change
|
||||||
|
pub fn push(&mut self, image: ImageValue) -> bool {
|
||||||
|
match image.validate_image() {
|
||||||
|
Ok(_) => self.set.insert(image),
|
||||||
|
Err(err) => {
|
||||||
|
admin_error!(
|
||||||
|
"Image didn't pass validation, not adding to value! Error: {}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_dbvs2(data: &[DbValueImage]) -> Result<ValueSet, OperationError> {
|
||||||
|
Ok(Box::new(ValueSetImage {
|
||||||
|
set: data
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|e| match e {
|
||||||
|
DbValueImage::V1 {
|
||||||
|
filename,
|
||||||
|
filetype,
|
||||||
|
contents,
|
||||||
|
} => ImageValue::new(filename, filetype, contents),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_repl_v1(data: &[DbValueImage]) -> Result<ValueSet, OperationError> {
|
||||||
|
let mut set: HashSet<ImageValue> = HashSet::new();
|
||||||
|
for image in data {
|
||||||
|
let image = match image.clone() {
|
||||||
|
DbValueImage::V1 {
|
||||||
|
filename,
|
||||||
|
filetype,
|
||||||
|
contents,
|
||||||
|
} => ImageValue::new(filename, filetype, contents),
|
||||||
|
};
|
||||||
|
match image.validate_image() {
|
||||||
|
Ok(_) => {
|
||||||
|
set.insert(image.clone());
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
admin_error!(
|
||||||
|
"Image didn't pass validation, not adding to value! Error: {:?}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
return Err(OperationError::InvalidValueState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Box::new(ValueSetImage { set }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to allow this, because rust doesn't allow us to impl FromIterator on foreign
|
||||||
|
// types, and `ImageValue` is foreign.
|
||||||
|
#[allow(clippy::should_implement_trait)]
|
||||||
|
pub fn from_iter<T>(iter: T) -> Option<Box<ValueSetImage>>
|
||||||
|
where
|
||||||
|
T: IntoIterator<Item = ImageValue>,
|
||||||
|
{
|
||||||
|
let mut set: HashSet<ImageValue> = HashSet::new();
|
||||||
|
for image in iter {
|
||||||
|
match image.validate_image() {
|
||||||
|
Ok(_) => set.insert(image),
|
||||||
|
Err(err) => {
|
||||||
|
admin_error!(
|
||||||
|
"Image didn't pass validation, not adding to value! Error: {}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Some(Box::new(ValueSetImage { set }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValueSetT for ValueSetImage {
|
||||||
|
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
||||||
|
match value {
|
||||||
|
Value::Image(image) => match self.set.contains(&image) {
|
||||||
|
true => Ok(false), // image exists, no change, return false
|
||||||
|
false => Ok(self.push(image)), // this masks the operationerror
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
debug_assert!(false);
|
||||||
|
Err(OperationError::InvalidValueState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear(&mut self) {
|
||||||
|
self.set.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove(&mut self, pv: &PartialValue, _cid: &Cid) -> bool {
|
||||||
|
match pv {
|
||||||
|
PartialValue::Image(pv) => {
|
||||||
|
let imgset = self.set.clone();
|
||||||
|
|
||||||
|
let res: Vec<bool> = imgset
|
||||||
|
.iter()
|
||||||
|
.filter_map(|image| {
|
||||||
|
if &image.hash_imagevalue() == pv {
|
||||||
|
Some(image)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(|image| self.set.remove(image))
|
||||||
|
.collect();
|
||||||
|
res.into_iter().any(|e| e)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
debug_assert!(false);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn contains(&self, pv: &PartialValue) -> bool {
|
||||||
|
match pv {
|
||||||
|
PartialValue::Image(pvhash) => {
|
||||||
|
if let Some(image) = self.set.iter().take(1).next() {
|
||||||
|
&image.hash_imagevalue() == pvhash
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn substring(&self, _pv: &PartialValue) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lessthan(&self, _pv: &PartialValue) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn len(&self) -> usize {
|
||||||
|
self.set.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_idx_eq_keys(&self) -> Vec<String> {
|
||||||
|
self.set
|
||||||
|
.iter()
|
||||||
|
.map(|image| image.hash_imagevalue())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn syntax(&self) -> SyntaxType {
|
||||||
|
SyntaxType::Image
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate(&self, schema_attr: &SchemaAttribute) -> bool {
|
||||||
|
if !schema_attr.multivalue && self.set.len() > 1 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.set.iter().all(|image| {
|
||||||
|
image
|
||||||
|
.validate_image()
|
||||||
|
.map_err(|err| error!("Image {} failed validation: {}", image.filename, err))
|
||||||
|
.is_ok()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
|
||||||
|
Box::new(self.set.iter().map(|image| image.hash_imagevalue()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
|
||||||
|
DbValueSetV2::Image(
|
||||||
|
self.set
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|e| crate::be::dbvalue::DbValueImage::V1 {
|
||||||
|
filename: e.filename,
|
||||||
|
filetype: e.filetype,
|
||||||
|
contents: e.contents,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_repl_v1(&self) -> ReplAttrV1 {
|
||||||
|
ReplAttrV1::Image {
|
||||||
|
set: self
|
||||||
|
.set
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|e| DbValueImage::V1 {
|
||||||
|
filename: e.filename,
|
||||||
|
filetype: e.filetype,
|
||||||
|
contents: e.contents,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_partialvalue_iter(&self) -> Box<dyn Iterator<Item = PartialValue> + '_> {
|
||||||
|
Box::new(
|
||||||
|
self.set
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|image| PartialValue::Image(image.hash_imagevalue())),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_value_iter(&self) -> Box<dyn Iterator<Item = Value> + '_> {
|
||||||
|
Box::new(self.set.iter().cloned().map(Value::Image))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn equal(&self, other: &ValueSet) -> bool {
|
||||||
|
if let Some(other) = other.as_imageset() {
|
||||||
|
&self.set == other
|
||||||
|
} else {
|
||||||
|
debug_assert!(false);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge(&mut self, other: &ValueSet) -> Result<(), OperationError> {
|
||||||
|
if let Some(b) = other.as_imageset() {
|
||||||
|
mergesets!(self.set, b)
|
||||||
|
} else {
|
||||||
|
debug_assert!(false);
|
||||||
|
Err(OperationError::InvalidValueState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this seems dumb
|
||||||
|
fn as_imageset(&self) -> Option<&HashSet<ImageValue>> {
|
||||||
|
Some(&self.set)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// tests that we can load a bunch of test images and it'll throw errors in a way we expect
|
||||||
|
fn test_imagevalue_things() {
|
||||||
|
["gif", "png", "jpg", "webp"]
|
||||||
|
.into_iter()
|
||||||
|
.for_each(|extension| {
|
||||||
|
// test should-be-bad images
|
||||||
|
let filename = format!(
|
||||||
|
"{}/src/valueset/image/test_images/oversize_dimensions.{extension}",
|
||||||
|
env!("CARGO_MANIFEST_DIR")
|
||||||
|
);
|
||||||
|
trace!("testing {}", &filename);
|
||||||
|
let image = ImageValue {
|
||||||
|
filename: format!("oversize_dimensions.{extension}"),
|
||||||
|
filetype: ImageType::try_from(extension).unwrap(),
|
||||||
|
contents: std::fs::read(filename).unwrap(),
|
||||||
|
};
|
||||||
|
let res = image.validate_image();
|
||||||
|
trace!("{:?}", &res);
|
||||||
|
assert!(res.is_err());
|
||||||
|
|
||||||
|
// test should-be-good images
|
||||||
|
let filename = format!(
|
||||||
|
"{}/src/valueset/image/test_images/ok.{extension}",
|
||||||
|
env!("CARGO_MANIFEST_DIR")
|
||||||
|
);
|
||||||
|
trace!("testing {}", &filename);
|
||||||
|
let image = ImageValue {
|
||||||
|
filename: filename.clone(),
|
||||||
|
filetype: ImageType::try_from(extension).unwrap(),
|
||||||
|
contents: std::fs::read(filename).unwrap(),
|
||||||
|
};
|
||||||
|
let res = image.validate_image();
|
||||||
|
trace!("validation result of {}: {:?}", image.filename, &res);
|
||||||
|
assert!(res.is_ok());
|
||||||
|
|
||||||
|
let filename = format!(
|
||||||
|
"{}/src/valueset/image/test_images/ok.svg",
|
||||||
|
env!("CARGO_MANIFEST_DIR")
|
||||||
|
);
|
||||||
|
let image = ImageValue {
|
||||||
|
filename: filename.clone(),
|
||||||
|
filetype: ImageType::Svg,
|
||||||
|
contents: std::fs::read(&filename).unwrap(),
|
||||||
|
};
|
||||||
|
let res = image.validate_image();
|
||||||
|
trace!("SVG Validation result of {}: {:?}", filename, &res);
|
||||||
|
assert!(res.is_ok());
|
||||||
|
assert_eq!(image.hash_imagevalue().is_empty(), false);
|
||||||
|
})
|
||||||
|
}
|
158
server/lib/src/valueset/image/png.rs
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
use super::{ImageValidationError, MAX_IMAGE_HEIGHT, MAX_IMAGE_WIDTH};
|
||||||
|
use crate::prelude::*;
|
||||||
|
static PNG_PRELUDE: &[u8] = &[0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
|
||||||
|
static PNG_CHUNK_END: &[u8; 4] = b"IEND";
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
/// This is used as part of PNG validation to identify if we've seen the end of the file, and if it suffers from
|
||||||
|
/// Acropalypyse issues by having trailing data.
|
||||||
|
enum PngChunkStatus {
|
||||||
|
SeenEnd { has_trailer: bool },
|
||||||
|
MoreChunks,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loop over the PNG file contents to find out if we've got valid chunks
|
||||||
|
fn png_consume_chunks_until_iend(
|
||||||
|
buf: &[u8],
|
||||||
|
) -> Result<(PngChunkStatus, &[u8]), ImageValidationError> {
|
||||||
|
// length[u8;4] + chunk_type[u8;4] + checksum[u8;4] + minimum size
|
||||||
|
if buf.len() < 12 {
|
||||||
|
return Err(ImageValidationError::InvalidImage(format!(
|
||||||
|
"PNG file is too short to be valid, got {} bytes",
|
||||||
|
buf.len()
|
||||||
|
)));
|
||||||
|
} else {
|
||||||
|
#[cfg(any(debug_assertions, test))]
|
||||||
|
trace!("input buflen: {}", buf.len());
|
||||||
|
}
|
||||||
|
let (length_bytes, buf) = buf.split_at(4);
|
||||||
|
let (chunk_type, buf) = buf.split_at(4);
|
||||||
|
|
||||||
|
// Infallible: We've definitely consumed 4 bytes
|
||||||
|
let length = u32::from_be_bytes(
|
||||||
|
length_bytes
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| ImageValidationError::InvalidImage("PNG corrupt!".to_string()))?,
|
||||||
|
);
|
||||||
|
#[cfg(any(debug_assertions, test))]
|
||||||
|
trace!(
|
||||||
|
"length_bytes: {:?} length: {} chunk_type: {:?} buflen: {}",
|
||||||
|
length_bytes,
|
||||||
|
&length,
|
||||||
|
&chunk_type,
|
||||||
|
&buf.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
if buf.len() < (length + 4) as usize {
|
||||||
|
return Err(ImageValidationError::InvalidImage(format!(
|
||||||
|
"PNG file is too short to be valid, failed to split at the chunk length {}, had {} bytes",
|
||||||
|
length,
|
||||||
|
buf.len(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let (_, buf) = buf.split_at(length as usize);
|
||||||
|
#[cfg(any(debug_assertions, test))]
|
||||||
|
trace!("new buflen: {}", &buf.len());
|
||||||
|
|
||||||
|
let (_checksum, buf) = buf.split_at(4);
|
||||||
|
#[cfg(any(debug_assertions, test))]
|
||||||
|
trace!("post-checksum buflen: {}", &buf.len());
|
||||||
|
|
||||||
|
if chunk_type == PNG_CHUNK_END {
|
||||||
|
if buf.is_empty() {
|
||||||
|
Ok((PngChunkStatus::SeenEnd { has_trailer: false }, buf))
|
||||||
|
} else {
|
||||||
|
Ok((PngChunkStatus::SeenEnd { has_trailer: true }, buf))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok((PngChunkStatus::MoreChunks, buf))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// needs to be pub for bench things
|
||||||
|
pub fn png_has_trailer(contents: &Vec<u8>) -> Result<bool, ImageValidationError> {
|
||||||
|
let buf = contents.as_slice();
|
||||||
|
// let magic = buf.split_off(PNG_PRELUDE.len());
|
||||||
|
let (magic, buf) = buf.split_at(PNG_PRELUDE.len());
|
||||||
|
|
||||||
|
let buf = buf.to_owned();
|
||||||
|
let mut buf = buf.as_slice();
|
||||||
|
|
||||||
|
if magic != PNG_PRELUDE {
|
||||||
|
return Err(ImageValidationError::InvalidPngPrelude);
|
||||||
|
}
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let (status, new_buf) = png_consume_chunks_until_iend(buf)?;
|
||||||
|
buf = match status {
|
||||||
|
PngChunkStatus::SeenEnd { has_trailer } => return Ok(has_trailer),
|
||||||
|
PngChunkStatus::MoreChunks => new_buf,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// needs to be pub for bench things
|
||||||
|
pub fn png_lodepng_validate(
|
||||||
|
contents: &Vec<u8>,
|
||||||
|
filename: &String,
|
||||||
|
) -> Result<(), ImageValidationError> {
|
||||||
|
match lodepng::decode32(contents) {
|
||||||
|
Ok(val) => {
|
||||||
|
if val.width > MAX_IMAGE_WIDTH as usize || val.height > MAX_IMAGE_HEIGHT as usize {
|
||||||
|
admin_debug!(
|
||||||
|
"PNG validation failed for {} {}",
|
||||||
|
filename,
|
||||||
|
ImageValidationError::ExceedsMaxWidth
|
||||||
|
);
|
||||||
|
Err(ImageValidationError::ExceedsMaxWidth)
|
||||||
|
} else if val.height > MAX_IMAGE_HEIGHT as usize {
|
||||||
|
admin_debug!(
|
||||||
|
"PNG validation failed for {} {}",
|
||||||
|
filename,
|
||||||
|
ImageValidationError::ExceedsMaxHeight
|
||||||
|
);
|
||||||
|
Err(ImageValidationError::ExceedsMaxHeight)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
// admin_debug!("PNG validation failed for {} {:?}", self.filename, err);
|
||||||
|
Err(ImageValidationError::InvalidImage(format!("{:?}", err)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// this tests a variety of input options for `png_consume_chunks_until_iend`
|
||||||
|
fn test_png_consume_chunks_until_iend() {
|
||||||
|
let mut foo = vec![0, 0, 0, 1]; // the length
|
||||||
|
|
||||||
|
foo.extend(PNG_CHUNK_END); // ... the type of chunk we're looking at!
|
||||||
|
foo.push(1); // the data
|
||||||
|
foo.extend([0, 0, 0, 1]); // the 4-byte checksum which we ignore
|
||||||
|
let expected: [u8; 0] = [];
|
||||||
|
let foo = foo.as_slice();
|
||||||
|
let res = png_consume_chunks_until_iend(&foo);
|
||||||
|
|
||||||
|
// simple, valid image works
|
||||||
|
match res {
|
||||||
|
Ok((result, buf)) => {
|
||||||
|
if let PngChunkStatus::MoreChunks = result {
|
||||||
|
panic!("Shouldn't have more chunks!");
|
||||||
|
}
|
||||||
|
assert_eq!(buf, &expected);
|
||||||
|
}
|
||||||
|
Err(err) => panic!("Error: {:?}", err),
|
||||||
|
};
|
||||||
|
|
||||||
|
// let's make sure it works with a bunch of different length inputs
|
||||||
|
let mut x = 11;
|
||||||
|
while x > 0 {
|
||||||
|
let foo = &foo[0..=x];
|
||||||
|
let res = png_consume_chunks_until_iend(&foo);
|
||||||
|
trace!("chunkstatus at size {} {:?}", x, &res);
|
||||||
|
assert!(res.is_err());
|
||||||
|
x = x - 1;
|
||||||
|
}
|
||||||
|
}
|
BIN
server/lib/src/valueset/image/test_images/ok.gif
Normal file
After Width: | Height: | Size: 61 KiB |
BIN
server/lib/src/valueset/image/test_images/ok.jpg
Normal file
After Width: | Height: | Size: 84 KiB |
BIN
server/lib/src/valueset/image/test_images/ok.png
Normal file
After Width: | Height: | Size: 95 KiB |
834
server/lib/src/valueset/image/test_images/ok.svg
Normal file
|
@ -0,0 +1,834 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
id="svg5800"
|
||||||
|
version="1.1"
|
||||||
|
viewBox="0 0 39.6875 39.6875"
|
||||||
|
height="150"
|
||||||
|
width="150"
|
||||||
|
sodipodi:docname="icon-accounts.svg"
|
||||||
|
inkscape:version="1.2.1 (9c6d41e, 2022-07-14)"
|
||||||
|
inkscape:export-filename="../94e4957b/kani-yellow-sign.png"
|
||||||
|
inkscape:export-xdpi="86.699997"
|
||||||
|
inkscape:export-ydpi="86.699997"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1391"
|
||||||
|
inkscape:window-height="819"
|
||||||
|
id="namedview108"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="2.2181916"
|
||||||
|
inkscape:cx="99.179892"
|
||||||
|
inkscape:cy="83.175864"
|
||||||
|
inkscape:window-x="49"
|
||||||
|
inkscape:window-y="25"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="layer3"
|
||||||
|
inkscape:showpageshadow="0"
|
||||||
|
inkscape:pagecheckerboard="1"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
showguides="true">
|
||||||
|
<sodipodi:guide
|
||||||
|
position="0,-35.312501"
|
||||||
|
orientation="0,1"
|
||||||
|
id="guide932"
|
||||||
|
inkscape:locked="false"
|
||||||
|
inkscape:label="MiddleHorizontal"
|
||||||
|
inkscape:color="rgb(222,221,218)" />
|
||||||
|
<sodipodi:guide
|
||||||
|
position="75,39.687501"
|
||||||
|
orientation="-1,0"
|
||||||
|
id="guide934"
|
||||||
|
inkscape:locked="false"
|
||||||
|
inkscape:label="MiddleVertical"
|
||||||
|
inkscape:color="rgb(222,221,218)" />
|
||||||
|
<sodipodi:guide
|
||||||
|
position="4.7580686,-10.089218"
|
||||||
|
orientation="0,-1"
|
||||||
|
id="guide1652"
|
||||||
|
inkscape:locked="false" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<defs
|
||||||
|
id="defs5794">
|
||||||
|
<radialGradient
|
||||||
|
id="Gradient_4"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
cx="420.20801"
|
||||||
|
cy="346.897"
|
||||||
|
r="21.1">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
stop-color="#B1DEF4"
|
||||||
|
id="stop8111" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#10A7CE"
|
||||||
|
id="stop8113" />
|
||||||
|
</radialGradient>
|
||||||
|
<radialGradient
|
||||||
|
id="Gradient_5"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
cx="385.79401"
|
||||||
|
cy="98.971001"
|
||||||
|
r="165.23399">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
stop-color="#B1DEF4"
|
||||||
|
id="stop8116" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#10A7CE"
|
||||||
|
id="stop8118" />
|
||||||
|
</radialGradient>
|
||||||
|
<radialGradient
|
||||||
|
id="Gradient_6"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
cx="154.649"
|
||||||
|
cy="435.46399"
|
||||||
|
r="21.1">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
stop-color="#F7B6D3"
|
||||||
|
id="stop8121" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#FF5D73"
|
||||||
|
id="stop8123" />
|
||||||
|
</radialGradient>
|
||||||
|
<radialGradient
|
||||||
|
id="Gradient_7"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
cx="120.117"
|
||||||
|
cy="187.53799"
|
||||||
|
r="165.23399">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
stop-color="#F7B6D3"
|
||||||
|
id="stop8126" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#FF5D73"
|
||||||
|
id="stop8128" />
|
||||||
|
</radialGradient>
|
||||||
|
<radialGradient
|
||||||
|
id="Gradient_8"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
cx="660.41699"
|
||||||
|
cy="435.46399"
|
||||||
|
r="21.1">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
stop-color="#F7AEB4"
|
||||||
|
id="stop8131" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#F60012"
|
||||||
|
id="stop8133" />
|
||||||
|
</radialGradient>
|
||||||
|
<radialGradient
|
||||||
|
id="Gradient_9"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
cx="625.88501"
|
||||||
|
cy="187.53799"
|
||||||
|
r="165.23399">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
stop-color="#F7AEB4"
|
||||||
|
id="stop8136" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#F60012"
|
||||||
|
id="stop8138" />
|
||||||
|
</radialGradient>
|
||||||
|
<radialGradient
|
||||||
|
id="Gradient_10"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
cx="283.38901"
|
||||||
|
cy="577.77899"
|
||||||
|
r="21.1">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
stop-color="#FBDEAD"
|
||||||
|
id="stop8141" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#FF9800"
|
||||||
|
id="stop8143" />
|
||||||
|
</radialGradient>
|
||||||
|
<radialGradient
|
||||||
|
id="Gradient_11"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
cx="213.711"
|
||||||
|
cy="337.353"
|
||||||
|
r="165.23399">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
stop-color="#FBDEAD"
|
||||||
|
id="stop8146" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#FF9800"
|
||||||
|
id="stop8148" />
|
||||||
|
</radialGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="Gradient_12"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="395.94601"
|
||||||
|
y1="909.60699"
|
||||||
|
x2="398.86099"
|
||||||
|
y2="889.30902">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
stop-color="#FF1F19"
|
||||||
|
id="stop8151" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#B7282C"
|
||||||
|
id="stop8153" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="Gradient_13"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="364.41199"
|
||||||
|
y1="920.81097"
|
||||||
|
x2="381.88199"
|
||||||
|
y2="898.88202">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
stop-color="#FF1F19"
|
||||||
|
id="stop8156" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#B7282C"
|
||||||
|
id="stop8158" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="Gradient_14"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="400.103"
|
||||||
|
y1="884.01398"
|
||||||
|
x2="414.327"
|
||||||
|
y2="886.43201">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
stop-color="#FF1F19"
|
||||||
|
id="stop8161" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#B7282C"
|
||||||
|
id="stop8163" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="Gradient_15"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="401.52899"
|
||||||
|
y1="-883.02899"
|
||||||
|
x2="418.35501"
|
||||||
|
y2="-884.72101"
|
||||||
|
gradientTransform="scale(1,-1)">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
stop-color="#AF2525"
|
||||||
|
id="stop8166" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#90282C"
|
||||||
|
id="stop8168" />
|
||||||
|
</linearGradient>
|
||||||
|
<clipPath
|
||||||
|
id="Clip_1">
|
||||||
|
<path
|
||||||
|
d="m 408.16,886.73 c 2.63,0.16 5.1,-0.27 7.57,-1.06 3.52,-1.3 2.99,-1.72 1.65,-1.82 v 0 l 0.21,-2.29 c -2.74,0.34 -5.51,-0.08 -8.21,-0.56 v 0 c 1.75,0.6 3.93,1.12 4.79,2.94 -0.02,0 -0.03,0 -0.03,0 0,0 -3.11,3.22 -12.86,1.6 2.22,0.78 4.56,0.96 6.88,1.19 z"
|
||||||
|
id="path8171" />
|
||||||
|
</clipPath>
|
||||||
|
<linearGradient
|
||||||
|
id="Gradient_16"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="406.53"
|
||||||
|
y1="880.51599"
|
||||||
|
x2="409.40302"
|
||||||
|
y2="880.80499">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
stop-color="#AF2525"
|
||||||
|
id="stop8174" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#90282C"
|
||||||
|
id="stop8176" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="Gradient_17"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="452.20001"
|
||||||
|
y1="905.45599"
|
||||||
|
x2="444.604"
|
||||||
|
y2="892.92102">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
stop-color="#FF1F19"
|
||||||
|
id="stop8179" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#B7282C"
|
||||||
|
id="stop8181" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="Gradient_18"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="462.254"
|
||||||
|
y1="893.729"
|
||||||
|
x2="456.45901"
|
||||||
|
y2="887.92401">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
stop-color="#FF1F19"
|
||||||
|
id="stop8184" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#B7282C"
|
||||||
|
id="stop8186" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="Gradient_19"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="446.13501"
|
||||||
|
y1="882.91602"
|
||||||
|
x2="429.77499"
|
||||||
|
y2="883.61603">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
stop-color="#FF1F19"
|
||||||
|
id="stop8189" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#B7282C"
|
||||||
|
id="stop8191" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="Gradient_20"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="443.24799"
|
||||||
|
y1="-882.92798"
|
||||||
|
x2="426.452"
|
||||||
|
y2="-884.64899"
|
||||||
|
gradientTransform="scale(1,-1)">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
stop-color="#AF2525"
|
||||||
|
id="stop8194" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#90282C"
|
||||||
|
id="stop8196" />
|
||||||
|
</linearGradient>
|
||||||
|
<clipPath
|
||||||
|
id="Clip_2">
|
||||||
|
<path
|
||||||
|
d="m 436.63,886.65 c 2.32,-0.16 4.66,-0.45 6.88,-1.19 -3.99,0.52 -9.46,1.2 -12.88,-1.55 0,0 -0.01,0.01 -0.03,0 0.38,-1.36 2.78,-2.34 4.78,-2.94 -5.2,1.07 -8.18,0.51 -8.18,0.51 l 0.23,2.22 0.01,0.01 c -1.35,0.13 -1.9,0.59 1.62,1.88 2.44,0.98 4.99,0.96 7.57,1.06 z"
|
||||||
|
id="path8199" />
|
||||||
|
</clipPath>
|
||||||
|
<linearGradient
|
||||||
|
id="Gradient_21"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="438.23401"
|
||||||
|
y1="880.48499"
|
||||||
|
x2="435.36099"
|
||||||
|
y2="880.78003">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
stop-color="#AF2525"
|
||||||
|
id="stop8202" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#90282C"
|
||||||
|
id="stop8204" />
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient
|
||||||
|
id="Gradient_22"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
cx="421.086"
|
||||||
|
cy="879.30603"
|
||||||
|
r="5.8280001">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
stop-color="#E61F19"
|
||||||
|
id="stop8207" />
|
||||||
|
<stop
|
||||||
|
offset="0.988"
|
||||||
|
stop-color="#9E282C"
|
||||||
|
id="stop8209" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#9E282C"
|
||||||
|
id="stop8211" />
|
||||||
|
</radialGradient>
|
||||||
|
<radialGradient
|
||||||
|
id="Gradient_23"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
cx="146.832"
|
||||||
|
cy="-316.11899"
|
||||||
|
r="53.613998"
|
||||||
|
gradientTransform="scale(1,-1)">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
stop-color="#FFFFFF"
|
||||||
|
id="stop8214" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#FFFFFF"
|
||||||
|
stop-opacity="0.001"
|
||||||
|
id="stop8216" />
|
||||||
|
</radialGradient>
|
||||||
|
<radialGradient
|
||||||
|
id="Gradient_24"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
cx="55.174999"
|
||||||
|
cy="-176.464"
|
||||||
|
r="53.613998"
|
||||||
|
gradientTransform="scale(1,-1)">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
stop-color="#FFFFFF"
|
||||||
|
id="stop8219" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#FFFFFF"
|
||||||
|
stop-opacity="0.001"
|
||||||
|
id="stop8221" />
|
||||||
|
</radialGradient>
|
||||||
|
<radialGradient
|
||||||
|
id="Gradient_25"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
cx="565.53101"
|
||||||
|
cy="-165.11301"
|
||||||
|
r="53.613998"
|
||||||
|
gradientTransform="scale(1,-1)">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
stop-color="#FFFFFF"
|
||||||
|
id="stop8224" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#FFFFFF"
|
||||||
|
stop-opacity="0.001"
|
||||||
|
id="stop8226" />
|
||||||
|
</radialGradient>
|
||||||
|
<radialGradient
|
||||||
|
id="Gradient_26"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
cx="333.47"
|
||||||
|
cy="-63.138"
|
||||||
|
r="53.613998"
|
||||||
|
gradientTransform="scale(1,-1)">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
stop-color="#FFFFFF"
|
||||||
|
id="stop8229" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#FFFFFF"
|
||||||
|
stop-opacity="0.001"
|
||||||
|
id="stop8231" />
|
||||||
|
</radialGradient>
|
||||||
|
<radialGradient
|
||||||
|
id="Gradient_27"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
cx="536.58099"
|
||||||
|
cy="578.03198"
|
||||||
|
r="21.1">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
stop-color="#C0E7C7"
|
||||||
|
id="stop8234" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#229614"
|
||||||
|
id="stop8236" />
|
||||||
|
</radialGradient>
|
||||||
|
<radialGradient
|
||||||
|
id="Gradient_28"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
cx="553.57397"
|
||||||
|
cy="328.29001"
|
||||||
|
r="165.23399">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
stop-color="#C0E7C7"
|
||||||
|
id="stop8239" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#229614"
|
||||||
|
id="stop8241" />
|
||||||
|
</radialGradient>
|
||||||
|
<radialGradient
|
||||||
|
id="Gradient_29"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
cx="488.81201"
|
||||||
|
cy="-304.62601"
|
||||||
|
r="53.613998"
|
||||||
|
gradientTransform="scale(1,-1)">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
stop-color="#FFFFFF"
|
||||||
|
id="stop8244" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#FFFFFF"
|
||||||
|
stop-opacity="0.001"
|
||||||
|
id="stop8246" />
|
||||||
|
</radialGradient>
|
||||||
|
<symbol
|
||||||
|
id="balloons"
|
||||||
|
viewBox="0 0 825 962">
|
||||||
|
<g
|
||||||
|
style="fill:none;stroke:#eccb78;stroke-width:4.069;stroke-linecap:round;stroke-linejoin:round"
|
||||||
|
id="g8261">
|
||||||
|
<path
|
||||||
|
d="M 291.13,584.61 422.41,889.24 540.38,589.26"
|
||||||
|
id="path8249" />
|
||||||
|
<path
|
||||||
|
d="m 426.95,357.75 -4.54,531.49"
|
||||||
|
id="path8251" />
|
||||||
|
<path
|
||||||
|
d="M 158.17,411.35 421.71,889.24 667.1,402.98"
|
||||||
|
id="path8253" />
|
||||||
|
<path
|
||||||
|
d="m 415.21,958.51 c 8.81,-23.5 6.08,-71.63 6.08,-71.63 -3.66,47 -29.02,70.1 -29.02,70.1"
|
||||||
|
id="path8255" />
|
||||||
|
<path
|
||||||
|
d="m 456.29,956.29 c 0,0 -26.52,-21.77 -32.58,-68.52 0,0 -0.26,48.21 9.75,71.23"
|
||||||
|
id="path8257" />
|
||||||
|
<path
|
||||||
|
d="m 404.06,957.77 c 0,0 13.62,-20.66 18.45,-69.2"
|
||||||
|
id="path8259" />
|
||||||
|
</g>
|
||||||
|
<path
|
||||||
|
style="fill:url(#Gradient_4)"
|
||||||
|
d="m 423.73,337.05 -21.46,35.03 c -1.86,3.04 0.32,6.93 3.89,6.93 v 0 h 41.71 c 3.51,0 5.7,-3.8 3.94,-6.83 v 0 l -20.25,-35.03 c -0.88,-1.51 -2.41,-2.28 -3.95,-2.28 v 0 c -1.5,0 -2.99,0.72 -3.88,2.18 z"
|
||||||
|
id="path8263" />
|
||||||
|
<path
|
||||||
|
style="fill:url(#Gradient_5)"
|
||||||
|
d="m 273.05,178.98 c 0,97.08 68.93,175.78 153.96,175.79 v 0 c 85.03,0 153.96,-78.7 153.97,-175.79 v 0 C 580.98,81.9 512.05,3.2 427.01,3.2 v 0 c -85.03,0 -153.96,78.7 -153.96,175.78 z"
|
||||||
|
id="path8265" />
|
||||||
|
<path
|
||||||
|
style="fill:url(#Gradient_6)"
|
||||||
|
d="m 158.17,425.62 -21.46,35.02 c -1.86,3.04 0.32,6.94 3.89,6.94 v 0 h 41.71 c 3.51,0 5.7,-3.8 3.94,-6.84 v 0 L 166,425.71 c -0.88,-1.51 -2.41,-2.27 -3.95,-2.27 v 0 c -1.5,0 -2.99,0.72 -3.88,2.18 z"
|
||||||
|
id="path8267" />
|
||||||
|
<path
|
||||||
|
style="opacity:0.95;fill:url(#Gradient_7)"
|
||||||
|
d="m 7.37,267.55 c 0,97.08 68.93,175.78 153.97,175.78 v 0 c 85.03,0 153.96,-78.7 153.96,-175.78 v 0 C 315.3,170.47 246.37,91.77 161.34,91.77 v 0 C 76.31,91.77 7.38,170.47 7.37,267.55 Z"
|
||||||
|
id="path8269" />
|
||||||
|
<path
|
||||||
|
style="fill:url(#Gradient_8)"
|
||||||
|
d="m 663.94,425.62 -21.46,35.02 c -1.86,3.04 0.33,6.94 3.89,6.94 v 0 h 41.71 c 3.51,0 5.7,-3.8 3.94,-6.84 v 0 l -20.25,-35.03 c -0.88,-1.51 -2.41,-2.27 -3.95,-2.27 v 0 c -1.5,0 -2.99,0.72 -3.88,2.18 z"
|
||||||
|
id="path8271" />
|
||||||
|
<path
|
||||||
|
style="opacity:0.95;fill:url(#Gradient_9)"
|
||||||
|
d="m 513.14,267.55 c 0,97.08 68.93,175.78 153.96,175.78 v 0 c 85.03,0 153.96,-78.7 153.97,-175.78 v 0 C 821.07,170.47 752.14,91.77 667.1,91.77 v 0 c -85.03,0 -153.96,78.7 -153.96,175.78 z"
|
||||||
|
id="path8273" />
|
||||||
|
<path
|
||||||
|
style="fill:url(#Gradient_10)"
|
||||||
|
d="m 285.46,567.53 -16.21,37.74 c -1.41,3.27 1.31,6.82 4.83,6.3 v 0 l 41.28,-5.97 c 3.47,-0.5 5.1,-4.58 2.93,-7.33 v 0 l -25.06,-31.76 c -0.93,-1.17 -2.25,-1.74 -3.57,-1.74 v 0 c -1.72,0 -3.42,0.96 -4.2,2.76 z"
|
||||||
|
id="path8275" />
|
||||||
|
<path
|
||||||
|
style="opacity:0.95;fill:url(#Gradient_11)"
|
||||||
|
d="m 240.79,236.67 c -84.15,12.18 -141.11,99.94 -127.2,196.02 v 0 c 13.9,96.08 93.39,164.1 177.54,151.92 v 0 c 84.15,-12.18 141.1,-99.94 127.21,-196.02 v 0 C 405.52,300 336.93,235.26 260.34,235.26 v 0 c -6.47,0 -12.99,0.46 -19.55,1.41 z"
|
||||||
|
id="path8277" />
|
||||||
|
<path
|
||||||
|
style="fill:url(#Gradient_27)"
|
||||||
|
d="M 542.05,569.11 513.87,599 c -2.44,2.59 -1.1,6.85 2.38,7.59 v 0 l 40.83,8.54 c 3.43,0.72 6.36,-2.55 5.25,-5.88 v 0 l -12.64,-38.43 c -0.66,-2.01 -2.48,-3.13 -4.35,-3.14 v 0 c -1.18,0 -2.38,0.46 -3.29,1.43 z"
|
||||||
|
id="path8279" />
|
||||||
|
<path
|
||||||
|
style="opacity:0.95;fill:url(#Gradient_28)"
|
||||||
|
d="m 426.83,383.51 c -19.89,95.02 31.46,186.18 114.68,203.6 v 0 c 83.23,17.42 166.82,-45.49 186.71,-140.52 v 0 C 748.11,351.57 696.76,260.41 613.54,242.99 v 0 c -9.34,-1.96 -18.68,-2.9 -27.94,-2.89 v 0 c -73.25,0 -141.11,59.05 -158.77,143.41 z"
|
||||||
|
id="path8281" />
|
||||||
|
<path
|
||||||
|
style="opacity:0.7;fill:url(#Gradient_23)"
|
||||||
|
d="m 126.74,409.71 c 0,0 29.93,-94.86 60.85,-110.42 19.92,-10.03 -4.23,-30.39 -27.01,-14.05 -6.7,5.14 -12.35,11.69 -17.06,18.66 -14.36,21.28 -19.56,46.01 -20.09,71.27 v 0.02 c -0.07,11.6 1.32,23.12 3.31,34.52 z"
|
||||||
|
id="path8283" />
|
||||||
|
<path
|
||||||
|
style="opacity:0.7;fill:url(#Gradient_24)"
|
||||||
|
d="m 30.74,269.01 c 0,0 34.33,-93.36 65.93,-107.45 20.37,-9.09 -2.81,-30.55 -26.32,-15.3 0,0 -51.89,32.72 -39.61,122.75 z"
|
||||||
|
id="path8285" />
|
||||||
|
<path
|
||||||
|
style="opacity:0.7;fill:url(#Gradient_25)"
|
||||||
|
d="m 537.49,256.64 c 0,0 37.96,-91.94 70.09,-104.8 20.71,-8.29 -1.61,-30.64 -25.71,-16.31 0,0 -53.13,30.67 -44.38,121.11 z"
|
||||||
|
id="path8287" />
|
||||||
|
<path
|
||||||
|
style="opacity:0.7;fill:url(#Gradient_26)"
|
||||||
|
d="m 305.43,154.66 c 0,0 37.96,-91.94 70.09,-104.79 20.71,-8.28 -1.61,-30.64 -25.71,-16.32 0,0 -53.13,30.67 -44.38,121.11 z"
|
||||||
|
id="path8289" />
|
||||||
|
<path
|
||||||
|
style="opacity:0.7;fill:url(#Gradient_29)"
|
||||||
|
d="m 454.08,393.82 c 0,0 44.65,-88.88 77.64,-99.32 21.27,-6.73 0.66,-30.67 -24.42,-18.17 0,0 -55.25,26.66 -53.22,117.49 z"
|
||||||
|
id="path8291" />
|
||||||
|
<path
|
||||||
|
style="fill:url(#Gradient_12)"
|
||||||
|
d="m 375.83,907.75 9.08,1.65 c -1.21,2.8 0.79,11.89 0.79,11.89 v 0 c 8.26,-15.42 34.5,-34.41 33.81,-33.91 v 0 l 0.71,-5.79 c -24.03,1.57 -44.39,26.16 -44.39,26.16 z"
|
||||||
|
id="path8293" />
|
||||||
|
<path
|
||||||
|
style="fill:url(#Gradient_13)"
|
||||||
|
d="m 380.2,872.6 c 13.6,7.74 40.82,5.99 40.82,5.99 v 0 c -22.8,-5.81 -32.93,-7.21 -37.4,-7.21 v 0 c -4.17,0 -3.42,1.22 -3.42,1.22 z"
|
||||||
|
id="path8295" />
|
||||||
|
<path
|
||||||
|
style="fill:url(#Gradient_14)"
|
||||||
|
d="m 380.45,890.39 c 7.74,5.62 42.01,-3.08 42.01,-3.09 v 0 l -0.09,-10.01 c -42.04,-1.73 -42.31,-5.05 -42.31,-5.05 v 0 z"
|
||||||
|
id="path8297" />
|
||||||
|
<g
|
||||||
|
clip-path="url(#Clip_1)"
|
||||||
|
opacity="0.24"
|
||||||
|
id="g8301">
|
||||||
|
<path
|
||||||
|
style="fill:url(#Gradient_15)"
|
||||||
|
d="m 408.16,886.73 c 2.63,0.16 5.1,-0.27 7.57,-1.06 3.52,-1.3 2.99,-1.72 1.65,-1.82 v 0 l 0.21,-2.29 c 0,0 -3,0.5 -8.21,-0.56 2.01,0.59 4.41,1.58 4.79,2.94 -0.02,0 -0.03,0 -0.03,0 0,0 -3.11,3.22 -12.86,1.6 2.22,0.78 4.56,0.96 6.88,1.19 z"
|
||||||
|
id="path8299" />
|
||||||
|
</g>
|
||||||
|
<path
|
||||||
|
style="fill:url(#Gradient_16)"
|
||||||
|
d="m 409.38,881 c -1.53,-0.45 -2.83,-0.68 -2.83,-0.68 v 0 c 1.01,0.28 1.95,0.5 2.83,0.68 z"
|
||||||
|
id="path8303" />
|
||||||
|
<path
|
||||||
|
style="fill:url(#Gradient_17)"
|
||||||
|
d="m 425.53,887.37 c 0,0 25.53,18.33 33.88,33.85 v 0 c 0,0 1.98,-9.1 0.76,-11.89 v 0 l 9.08,-1.67 c 0,0 -20.4,-24.55 -44.44,-26.08 v 0 z"
|
||||||
|
id="path8305" />
|
||||||
|
<path
|
||||||
|
style="fill:url(#Gradient_18)"
|
||||||
|
d="m 423.74,878.59 c 0,0 40.72,9.96 40.43,-5.69 v 0 c -0.01,-0.45 0.33,-1.72 -3.83,-1.73 v 0 c -4.58,0 -14.62,1.54 -36.6,7.42 z"
|
||||||
|
id="path8307" />
|
||||||
|
<path
|
||||||
|
style="fill:url(#Gradient_19)"
|
||||||
|
d="m 422.39,877.29 -0.07,10.01 c 0,0 34.28,8.63 42.01,3 v 0 l -0.11,-17.86 c 0,0 -0.29,1.43 -41.83,4.85 z"
|
||||||
|
id="path8309" />
|
||||||
|
<g
|
||||||
|
clip-path="url(#Clip_2)"
|
||||||
|
opacity="0.24"
|
||||||
|
id="g8313">
|
||||||
|
<path
|
||||||
|
style="fill:url(#Gradient_20)"
|
||||||
|
d="m 436.63,886.65 c 2.32,-0.16 4.66,-0.45 6.88,-1.19 -9.75,1.64 -12.88,-1.55 -12.88,-1.55 0,0 -0.01,0.01 -0.03,0 0.38,-1.36 2.78,-2.34 4.78,-2.94 -5.2,1.07 -8.18,0.51 -8.18,0.51 l 0.23,2.22 0.01,0.01 c -1.35,0.13 -1.9,0.59 1.62,1.88 2.44,0.98 4.99,0.96 7.57,1.06 z"
|
||||||
|
id="path8311" />
|
||||||
|
</g>
|
||||||
|
<path
|
||||||
|
style="fill:url(#Gradient_21)"
|
||||||
|
d="m 435.38,880.98 c 0.88,-0.18 1.83,-0.4 2.83,-0.69 v 0 c 0,0 -1.3,0.23 -2.83,0.69 z"
|
||||||
|
id="path8315" />
|
||||||
|
<path
|
||||||
|
style="fill:url(#Gradient_22)"
|
||||||
|
d="m 417.09,877.31 c 0,0 -1.77,8.19 0.74,11.61 v 0 l 9.25,-0.01 c 0,0 2.64,-7.59 0.88,-11.59 v 0 c 0,0 -4.62,-1 -7.84,-1 v 0 c -1.6,0 -2.86,0.25 -3.03,0.99 z"
|
||||||
|
id="path8317" />
|
||||||
|
</symbol>
|
||||||
|
</defs>
|
||||||
|
<metadata
|
||||||
|
id="metadata5797">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer2"
|
||||||
|
inkscape:label="Kani"
|
||||||
|
style="display:inline"
|
||||||
|
transform="translate(-40.228511,-7.4385631)">
|
||||||
|
<path
|
||||||
|
id="path5404"
|
||||||
|
d="m 47.256748,30.044057 c -1.108637,2.679174 1.907156,5.148881 1.920091,5.188787 -0.550848,-1.615849 -1.133032,-2.920248 -0.0623,-4.049623"
|
||||||
|
style="fill:#803300;stroke:#000000;stroke-width:0.32966;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
inkscape:label="kani-rear-claw-left"
|
||||||
|
inkscape:export-filename="../1d28c49c/party-kani.png"
|
||||||
|
inkscape:export-xdpi="160.64"
|
||||||
|
inkscape:export-ydpi="160.64" />
|
||||||
|
<path
|
||||||
|
style="fill:#803300;stroke:#000000;stroke-width:0.32966;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 72.279451,29.867841 c 1.108639,2.679173 -1.907154,5.148881 -1.920091,5.188782 0.550847,-1.615844 1.133031,-2.920243 0.0623,-4.049619"
|
||||||
|
id="path5408"
|
||||||
|
inkscape:label="kani-rear-claw-right" />
|
||||||
|
<path
|
||||||
|
id="path4529"
|
||||||
|
d="m 59.687987,15.86156 c -0.431403,-0.0051 -0.84422,0.775615 -1.612258,2.30322 -2.32472,-2.556767 -2.443632,-2.587721 -3.24461,0.873588 -0.03747,0.01568 -0.07486,0.03161 -0.112266,0.0477 -2.928143,-1.755243 -3.079434,-1.768972 -2.857219,1.661998 -0.07111,0.05273 -0.142154,0.10618 -0.212947,0.160477 -3.000566,-1.188615 -3.130304,-1.113719 -2.371021,2.179539 -0.03555,0.03847 -0.0712,0.07676 -0.106646,0.115675 -3.058205,-0.826252 -3.143772,-0.702098 -2.003221,2.524684 -0.04894,0.0702 -0.09751,0.141613 -0.146165,0.212776 -3.446985,-0.368477 -3.517796,-0.301154 -1.765405,2.91037 -0.0085,0.01589 -0.01734,0.03109 -0.02591,0.04702 -1.4122,0.675083 -1.879711,1.497912 -2.548875,2.602365 -0.05195,2.190449 2.292587,4.07824 5.663674,6.223636 -1.158177,-2.146516 -2.969363,-3.234648 -3.31684,-5.897064 0.508886,-0.957788 1.374276,-1.288645 2.117359,-1.71958 8.116214,4.736605 16.686002,4.791513 25.235776,-0.181428 0.745079,0.433512 1.615611,0.763192 2.126555,1.72486 -0.347475,2.662414 -2.158831,3.750377 -3.317007,5.896892 3.371082,-2.145394 5.715783,-4.033017 5.663843,-6.223466 -0.650598,-1.073811 -1.113187,-1.88104 -2.436098,-2.545467 1.652553,-3.038888 1.495242,-3.06749 -1.973239,-2.695722 -0.02938,-0.03798 -0.05866,-0.07643 -0.08808,-0.114138 1.198204,-3.382817 1.123036,-3.471219 -2.107139,-2.594874 0.81753,-3.529305 0.708286,-3.543698 -2.564551,-2.242913 0.246522,-3.72022 0.158214,-3.712493 -2.930134,-1.860122 -0.823543,-3.566516 -0.920717,-3.555903 -3.27698,-0.964219 -0.886279,-1.626423 -1.352579,-2.440599 -1.790618,-2.445808 z"
|
||||||
|
style="display:inline;fill:#ff6600;stroke:#000000;stroke-width:0.32966;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
inkscape:label="kani-body" />
|
||||||
|
<g
|
||||||
|
id="g5758"
|
||||||
|
transform="matrix(0.32966003,0,0,0.32966003,25.875957,-8.7851129)"
|
||||||
|
style="display:inline"
|
||||||
|
inkscape:label="kani-face">
|
||||||
|
<path
|
||||||
|
style="display:inline;fill:#d45500;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 98.808992,121.55238 c -0.682767,-0.0559 4.468828,10.17565 8.051908,0.17277 0.24133,-0.67372 -3.69223,2.90976 -8.051908,-0.17277 z"
|
||||||
|
id="path5399"
|
||||||
|
inkscape:label="kani-smile" />
|
||||||
|
<g
|
||||||
|
id="g351"
|
||||||
|
inkscape:label="kani-eye-left">
|
||||||
|
<path
|
||||||
|
style="fill:#000000;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="m 93.212672,111.5632 c -5.131386,10.99206 5.618332,14.73472 4.412511,4.11951 -0.319289,-3.2881 -2.328089,-7.17448 -4.412511,-4.11951 z"
|
||||||
|
id="path5415" />
|
||||||
|
<path
|
||||||
|
style="fill:#ffffff;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="m 95.394393,112.41429 c -1.183441,1.51896 -0.08172,3.83605 -0.09872,3.98526 1.674403,1.64132 1.61267,-4.77034 0.09872,-3.98526 z"
|
||||||
|
id="path5417" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g355"
|
||||||
|
inkscape:label="kani-eye-right">
|
||||||
|
<path
|
||||||
|
id="path5433"
|
||||||
|
d="m 109.6169,111.5632 c -5.13141,10.99206 5.61836,14.73472 4.41254,4.11951 -0.31929,-3.2881 -2.3281,-7.17448 -4.41254,-4.11951 z"
|
||||||
|
style="fill:#000000;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
||||||
|
<path
|
||||||
|
id="path5435"
|
||||||
|
d="m 111.79864,112.41429 c -1.18345,1.51896 -0.0817,3.83605 -0.0987,3.98526 1.67441,1.64132 1.61267,-4.77034 0.0987,-3.98526 z"
|
||||||
|
style="display:inline;fill:#ffffff;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path
|
||||||
|
id="path5389"
|
||||||
|
d="m 51.475333,28.920487 c -0.168261,0.0013 -0.280434,0.01794 -0.262006,0.02931 0,0 -3.011135,3.934858 0.102216,6.151064 a 3.2132327,2.7286249 37.793943 0 0 0.473761,3.106624 3.2132327,2.7286249 37.793943 0 0 3.48277,1.307827 l -1.174949,-2.836952 3.031834,0.242419 a 3.2132327,2.7286249 37.793943 0 0 -2.31634,-2.937459 3.2132327,2.7286249 37.793943 0 0 -2.310205,0.003 c -0.613929,-0.449677 -1.525896,-1.623909 -0.01345,-4.112595 0.500447,-0.823475 -0.508826,-0.95705 -1.013621,-0.953144 z"
|
||||||
|
style="display:inline;fill:#ff6600;fill-opacity:1;stroke:#000000;stroke-width:0.32966;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
inkscape:label="kani-claw-left-down" />
|
||||||
|
<path
|
||||||
|
id="path8665"
|
||||||
|
d="m 83.960168,109.11313 c -0.27485,0.43011 -0.4985,0.69147 -0.49798,0.62578 0,0 -14.965898,1.38701 -15.636208,-10.186101 a 9.7471106,8.2770874 84.335985 0 1 -7.216103,-6.22894 9.7471106,8.2770874 84.335985 0 1 2.259156,-11.0566 l 5.392134,7.59511 4.268531,-8.17939 a 9.7471106,8.2770874 84.335985 0 1 3.80891,10.68931 9.7471106,8.2770874 84.335985 0 1 -3.73468,5.92977 c 0.16465,2.302521 1.70979,6.539611 10.54286,6.669381 2.92274,0.043 1.63761,2.85111 0.81319,4.14154 z"
|
||||||
|
style="display:none;fill:#ff6600;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
inkscape:label="kani-claw-left-up" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer3"
|
||||||
|
inkscape:label="kani-claw-right-overlay"
|
||||||
|
style="display:inline"
|
||||||
|
transform="translate(-40.228511,-7.4385631)">
|
||||||
|
<path
|
||||||
|
d="m 74.643953,26.251952 c 1.156883,-1.026462 1.383369,-2.311371 1.047116,-3.752599 l -2.436647,1.263643 z"
|
||||||
|
style="display:inline;fill:#2a3455;fill-opacity:1;stroke:#000000;stroke-width:0.32966;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path523"
|
||||||
|
inkscape:label="under-kani-claw" />
|
||||||
|
<path
|
||||||
|
id="path5732-74"
|
||||||
|
d="m 71.753651,21.394807 c -0.719107,0.469439 -1.267535,1.208385 -1.51602,2.042676 -0.326817,1.104585 -0.08447,2.222568 0.632582,2.918124 -1.17999,3.220643 -3.314446,2.161056 -3.359552,2.118688 -0.362682,-0.282284 -1.70696,1.196822 0.175601,1.723889 2.940634,0.823316 4.211648,-1.804817 4.666738,-3.187938 0.808096,0.06183 1.654903,-0.257623 2.324887,-0.877051 1.011643,-0.938034 1.432188,-2.380843 1.04105,-3.571645 l -2.74009,1.385776 0.0019,-0.425981 z"
|
||||||
|
style="display:inline;fill:#ff6600;fill-opacity:1;stroke:#000000;stroke-width:0.32966;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||||
|
<path
|
||||||
|
style="display:inline;opacity:1;fill:#ff6600;fill-opacity:1;stroke:#000000;stroke-width:0.32966;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 71.753651,21.394807 c -0.719107,0.46944 -1.267535,1.208385 -1.51602,2.042676 -0.326817,1.104585 -0.08447,2.222568 0.632582,2.918125 -1.17999,3.220639 -3.314446,2.161052 -3.359552,2.118684 -0.362682,-0.282284 -1.70696,1.196821 0.175604,1.723888 2.940631,0.823316 4.211645,-1.804816 4.666735,-3.187934 0.808099,0.06183 1.654907,-0.257623 2.324891,-0.877051 1.011642,-0.938034 1.432189,-2.380843 1.04105,-3.571645 l -2.740091,1.385776 0.0019,-0.425981 z"
|
||||||
|
id="path5728" />
|
||||||
|
<path
|
||||||
|
id="path5732"
|
||||||
|
d="m 71.753651,21.394807 c -0.719107,0.46944 -1.267535,1.208385 -1.51602,2.042676 -0.326817,1.104585 -0.08447,2.222568 0.632582,2.918125 -1.17999,3.220639 -3.314446,2.161052 -3.359552,2.118684 -0.362682,-0.282284 -1.70696,1.196821 0.175604,1.723888 2.940631,0.823316 4.211645,-1.804816 4.666735,-3.187934 0.808099,0.06183 1.654907,-0.257623 2.324891,-0.877051 1.011642,-0.938034 1.432189,-2.380843 1.04105,-3.571645 l -2.740091,1.385776 0.0019,-0.425981 z"
|
||||||
|
style="display:inline;opacity:1;fill:#ff6600;fill-opacity:1;stroke:#000000;stroke-width:0.32966;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g7998"
|
||||||
|
inkscape:label="party-hat"
|
||||||
|
transform="matrix(1.0107117,0,0,0.94642879,-34.697969,-6.0098395)"
|
||||||
|
style="display:none">
|
||||||
|
<path
|
||||||
|
style="display:inline;fill:#1da1f2;fill-opacity:1;stroke:none;stroke-width:1.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 84.970159,82.659204 98.594291,41.592222 113.931,82.386454 c 0,0 -8.7088,2.847069 -15.025249,2.754354 -6.316442,-0.09272 -13.935592,-2.481604 -13.935592,-2.481604 z"
|
||||||
|
id="path7024"
|
||||||
|
inkscape:label="party-hat-background"
|
||||||
|
sodipodi:nodetypes="cccsc" />
|
||||||
|
<path
|
||||||
|
style="display:inline;fill:#ffd60f;fill-opacity:1;stroke:#ff00ff;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 90.866459,64.772976 -1.399384,3.650367 c 0,0 6.297977,-0.0079 9.604616,-2.229612 3.306639,-2.221751 6.641769,-4.763148 6.637539,-5.892882 -0.004,-1.129737 -1.2244,-3.398221 -1.2244,-3.398221 0,0 -1.76379,2.983434 -5.089689,4.759036 -3.325897,1.775606 -6.256176,3.317156 -8.528682,3.111312 z"
|
||||||
|
id="path7711"
|
||||||
|
inkscape:label="yellow-middle" />
|
||||||
|
<path
|
||||||
|
style="fill:#ffd60f;fill-opacity:1;stroke:#ff00ff;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 95.283815,50.496253 c 0,0 2.017636,-0.742584 2.746046,-1.209859 0.68409,-0.438843 2.540819,-1.493505 2.505569,-1.916931 -0.0353,-0.423423 0.90713,1.638551 0.90713,1.638551 0,0 -0.43327,0.543192 -1.447199,1.610006 -1.01394,1.066813 -1.72817,1.358505 -2.26456,1.622681 -0.53638,0.264174 -2.0866,0.861497 -2.636383,0.793816 -0.549783,-0.06768 0.189397,-2.538264 0.189397,-2.538264 z"
|
||||||
|
id="path7713"
|
||||||
|
sodipodi:nodetypes="csccsssc"
|
||||||
|
inkscape:label="yellow-top" />
|
||||||
|
<path
|
||||||
|
style="display:inline;fill:#ffd60f;fill-opacity:1;stroke:#ff00ff;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 109.92717,71.488689 c 0,0 -2.20481,3.007619 -3.4367,4.221512 -1.06044,1.044951 -3.3987,2.32693 -5.45873,3.108264 -2.060039,0.78133 -7.390321,2.031263 -10.319319,2.035505 -2.53475,0.0037 -3.102127,-0.17882 -4.727213,-0.614261 -1.010264,-0.270699 -0.801047,2.045289 -0.801047,2.045289 l 4.858605,2.209687 10.217314,-0.32013 c 0,0 3.08806,-0.85865 3.26326,-0.9226 0.17519,-0.06395 5.61136,-3.775927 5.91906,-4.176562 0.30769,-0.400637 1.97931,-2.854544 1.85876,-3.106917 -0.12055,-0.25237 -1.37399,-4.479787 -1.37399,-4.479787 z"
|
||||||
|
id="path7715"
|
||||||
|
sodipodi:nodetypes="csssscccsssc"
|
||||||
|
inkscape:label="yellow-bottom" />
|
||||||
|
<path
|
||||||
|
style="display:inline;fill:#ffffff;fill-opacity:1;stroke:#ff00ff;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 88.208385,72.747325 c 0,0 6.251797,0.04012 9.907566,-1.876691 3.875559,-2.032049 5.147419,-2.116642 8.954089,-7.814465 -0.0353,-0.423423 1.59724,3.704345 1.59724,3.704345 0,0 -2.99139,5.115034 -6.19574,6.653752 -4.079519,1.958969 -6.324966,2.675602 -8.934752,3.106262 -1.713747,0.282798 -4.8213,0.352969 -6.27215,-0.01279 -0.537127,-0.135411 0.943747,-3.760409 0.943747,-3.760409 z"
|
||||||
|
id="path7982"
|
||||||
|
sodipodi:nodetypes="csccssscc"
|
||||||
|
inkscape:label="white-middle" />
|
||||||
|
<path
|
||||||
|
style="fill:#ffffff;fill-opacity:1;stroke:#ff00ff;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 92.993027,57.289539 c 0,0 3.629855,-0.719631 5.482954,-2.090937 0.65332,-0.483462 3.662839,-3.285322 4.163269,-4.079419 -0.0353,-0.423423 0.98376,2.839616 0.98376,2.839616 0,0 -1.55545,1.919882 -2.68768,2.888013 -1.118619,0.956483 -3.276219,2.193755 -4.815637,2.748644 -1.634013,0.588985 -3.388075,0.798009 -3.937858,0.730328 -0.549783,-0.06768 0.811192,-3.036245 0.811192,-3.036245 z"
|
||||||
|
id="path7979"
|
||||||
|
sodipodi:nodetypes="csccsssc"
|
||||||
|
inkscape:label="white-top" />
|
||||||
|
<path
|
||||||
|
style="display:inline;fill:none;fill-opacity:1;stroke:#ff00ff;stroke-width:1.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 84.970159,82.659204 98.594291,41.592222 113.931,82.386454 c 0,0 -8.7088,2.847069 -15.025249,2.754354 -6.316442,-0.09272 -13.935592,-2.481604 -13.935592,-2.481604 z"
|
||||||
|
id="path508"
|
||||||
|
inkscape:label="party-hat-outline"
|
||||||
|
sodipodi:nodetypes="cccsc" />
|
||||||
|
<path
|
||||||
|
style="display:inline;fill:#000000;fill-opacity:1;stroke:#8f57a8;stroke-width:2.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 95.307706,40.797374 8.863844,3.5236 c -4.511069,-1.476791 -5.455019,1.171445 -8.289058,2.243988 0,0 5.051628,-7.95802 4.765018,-7.734177 -1.618949,1.264417 -2.259979,9.16058 -2.259979,9.16058 l -0.26088,-9.396127 3.698779,7.921822 -7.078677,-2.809491 8.818747,-2.23602 z"
|
||||||
|
id="path3171-3"
|
||||||
|
sodipodi:nodetypes="cccscccccc"
|
||||||
|
inkscape:label="pompom" />
|
||||||
|
<path
|
||||||
|
style="display:inline;fill:#0056ed;fill-opacity:1;stroke:#ae00ff;stroke-width:2.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 96.516104,39.086314 5.923406,7.476417 c -3.172509,-3.530708 -5.311942,-1.706696 -8.303144,-2.191314 0,0 8.348654,-4.375959 7.988584,-4.324981 -2.03392,0.28796 -6.529397,6.811072 -6.529397,6.811072 l 4.462297,-8.273038 -0.747319,8.710783 -4.732664,-5.966807 8.758203,2.462516 z"
|
||||||
|
id="path3171"
|
||||||
|
sodipodi:nodetypes="cccscccccc"
|
||||||
|
inkscape:label="pompom" />
|
||||||
|
<path
|
||||||
|
style="fill:#744eaa;fill-opacity:1;stroke:#8f57a8;stroke-width:0.1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 85.749242,80.805507 -1.299196,-0.793626 -0.72012,1.516668 -1.19957,0.02641 1.07959,1.592728 -0.479079,1.665647 1.727851,-0.04555 0.945116,1.102037 1.595689,-0.420439 1.013886,1.119783 1.616715,-0.611034 0.955484,0.869926 1.417278,-0.82042 1.399024,0.893951 1.62274,-0.936247 1.191522,0.880515 1.692439,-0.626057 1.62431,0.594614 1.273839,-0.730245 1.14228,0.675465 0.83977,-0.542118 1.7433,0.433922 1.37853,-0.883036 1.46376,0.789948 0.98369,-0.723702 1.56885,0.265388 0.83917,-1.064009 1.5817,0.538036 1.04635,-1.072595 1.45989,-0.386437 -0.0243,-1.46382 0.73064,-1.311775 -1.08662,-0.620199 -0.54503,-1.441178 -1.29888,0.821314 -1.72095,-0.803735 -0.89109,1.605976 -1.44802,-0.173747 -0.40144,0.954405 -1.25934,-0.74345 -1.14276,1.025692 -2.05145,-0.719347 c 0,0 -1.1538,1.054828 -1.21655,1.018337 -0.0627,-0.03649 -0.72908,-0.632037 -0.72908,-0.632037 l -2.781839,0.990357 -1.01136,-0.982973 -0.76464,0.897745 -1.265471,-0.508519 -1.392952,0.640072 -1.261957,-0.842494 -1.207529,1.021591 -2.137111,-1.168069 -0.83207,0.565255 -0.716719,-0.940683 -1.262123,0.222665 -0.854411,-1.073467 z"
|
||||||
|
id="path3419"
|
||||||
|
inkscape:label="purple-frizzy-bit" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g571"
|
||||||
|
transform="matrix(-0.37792196,-0.19227008,-0.19227008,0.37792196,160.95541,83.78303)"
|
||||||
|
inkscape:export-filename="../fe6a9b8b/kani-stabby.png"
|
||||||
|
inkscape:export-xdpi="370.63721"
|
||||||
|
inkscape:export-ydpi="370.63721"
|
||||||
|
style="display:none"
|
||||||
|
inkscape:label="knife-left">
|
||||||
|
<path
|
||||||
|
d="M 327.32922,104.16963 313.23927,90.079688 246.31471,157.00414 c 0,0 -6.09712,4.9021 -4.45506,8.97393 1.6429,4.07209 4.34482,5.77218 11.55969,6.84567 6.14218,0.99798 6.56385,-1.72199 9.06459,-9.07879 2.49908,-7.35679 3.92356,-2.6465 12.85268,-11.57524 8.93008,-8.9275 21.21345,-19.64556 26.56932,-29.0037 5.35834,-9.35496 4.43089,-10.42811 12.07206,-10.21327 4.21357,0.0716 4.88879,-0.46249 6.31789,-1.74872 1.42875,-1.28481 7.03334,-7.03439 7.03334,-7.03439"
|
||||||
|
style="fill:#c52a34;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||||
|
id="path561" />
|
||||||
|
<path
|
||||||
|
d="M 333.98084,110.82126 313.52961,90.369671 c 0,0 66.27001,-67.178418 73.76936,-74.221626 7.49688,-7.0446203 13.081,-13.0810013 23.41845,-16.14946258 -0.68086,6.58883098 -6.21418,20.98428158 -13.48493,31.66357158 -7.2711,10.681759 -36.32483,51.549656 -63.25165,79.159106"
|
||||||
|
style="fill:#b9b8b7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||||
|
id="path563" />
|
||||||
|
<path
|
||||||
|
d="m 404.21431,2.595027 c -6.52039,3.3330447 -11.1573,8.142464 -16.91534,13.553018 -7.49935,7.043208 -73.76936,74.221626 -73.76936,74.221626 l 15.67956,15.678859 c 26.92717,-27.608745 55.97949,-68.476642 63.25094,-79.158049 5.17807,-7.605536 9.46362,-17.0825591 11.7542,-24.295454"
|
||||||
|
style="fill:#999a99;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||||
|
id="path565" />
|
||||||
|
<path
|
||||||
|
d="m 255.68936,173.17156 c -0.61955,0 -1.31989,-0.0656 -2.11762,-0.19541 -4.67209,-0.69397 -7.69193,-1.90252 -9.11972,-3.20187 l 0.0139,-0.014 c 1.82658,1.5197 4.5489,2.40839 8.9534,3.06351 0.81082,0.13188 1.52216,0.19899 2.15166,0.19899 4.13826,0 4.74239,-2.8925 6.91293,-9.27778 2.49908,-7.35679 3.92356,-2.6465 12.85268,-11.57524 8.93008,-8.9275 21.21345,-19.64556 26.56932,-29.0037 4.98299,-8.6995 4.53108,-10.23726 10.58792,-10.23726 0.4565,0 0.94862,0.009 1.48414,0.024 0.24729,0.004 0.48295,0.006 0.70661,0.006 3.58916,0 4.26614,-0.54433 5.61128,-1.75507 1.42875,-1.28481 7.03334,-7.03439 7.03334,-7.03439 v 0 c 0,0 -5.6,5.75451 -7.02875,7.03933 -1.34549,1.21109 -2.01824,1.7593 -5.61058,1.7593 -0.2226,0 -0.45649,-0.002 -0.70238,-0.006 -0.53269,-0.0152 -1.0227,-0.0236 -1.47708,-0.0236 -6.06319,0 -5.60035,1.54375 -10.5851,10.24714 -5.35693,9.35778 -17.62054,20.09429 -26.55122,29.02274 -8.92845,8.92803 -10.31434,4.2562 -12.81342,11.61408 -2.17593,6.40046 -2.71998,9.34932 -6.87133,9.34932 m 71.63986,-69.00193 -8.63636,-8.63635 v 0 l 8.63636,8.63635"
|
||||||
|
style="fill:#cdccca;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||||
|
id="path567" />
|
||||||
|
<path
|
||||||
|
d="m 255.571,173.02273 c -0.6295,0 -1.34084,-0.0671 -2.15166,-0.19899 -4.4045,-0.65512 -7.12682,-1.54381 -8.9534,-3.06351 l 74.22692,-74.22695 8.63636,8.63635 c 0,0 -5.60459,5.74958 -7.03334,7.03439 -1.34514,1.21074 -2.02212,1.75507 -5.61128,1.75507 -0.22366,0 -0.45932,-0.002 -0.70661,-0.006 -0.53552,-0.0152 -1.02764,-0.024 -1.48414,-0.024 -6.05684,0 -5.60493,1.53776 -10.58792,10.23726 -5.35587,9.35814 -17.63924,20.0762 -26.56932,29.0037 -8.92912,8.92874 -10.3536,4.21845 -12.85268,11.57524 -2.17054,6.38528 -2.77467,9.27778 -6.91293,9.27778"
|
||||||
|
style="fill:#9b232b;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
|
||||||
|
id="path569" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 39 KiB |
BIN
server/lib/src/valueset/image/test_images/ok.webp
Normal file
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 44 KiB |
After Width: | Height: | Size: 148 KiB |
After Width: | Height: | Size: 117 KiB |
After Width: | Height: | Size: 120 KiB |
After Width: | Height: | Size: 45 KiB |
|
@ -3,6 +3,7 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||||
use compact_jwt::JwsSigner;
|
use compact_jwt::JwsSigner;
|
||||||
use dyn_clone::DynClone;
|
use dyn_clone::DynClone;
|
||||||
use hashbrown::HashSet;
|
use hashbrown::HashSet;
|
||||||
|
use kanidm_proto::internal::ImageValue;
|
||||||
use openssl::ec::EcKey;
|
use openssl::ec::EcKey;
|
||||||
use openssl::pkey::Private;
|
use openssl::pkey::Private;
|
||||||
use openssl::pkey::Public;
|
use openssl::pkey::Public;
|
||||||
|
@ -30,6 +31,7 @@ pub use self::cid::ValueSetCid;
|
||||||
pub use self::cred::{ValueSetCredential, ValueSetDeviceKey, ValueSetIntentToken, ValueSetPasskey};
|
pub use self::cred::{ValueSetCredential, ValueSetDeviceKey, ValueSetIntentToken, ValueSetPasskey};
|
||||||
pub use self::datetime::ValueSetDateTime;
|
pub use self::datetime::ValueSetDateTime;
|
||||||
pub use self::eckey::ValueSetEcKeyPrivate;
|
pub use self::eckey::ValueSetEcKeyPrivate;
|
||||||
|
use self::image::ValueSetImage;
|
||||||
pub use self::iname::ValueSetIname;
|
pub use self::iname::ValueSetIname;
|
||||||
pub use self::index::ValueSetIndex;
|
pub use self::index::ValueSetIndex;
|
||||||
pub use self::iutf8::ValueSetIutf8;
|
pub use self::iutf8::ValueSetIutf8;
|
||||||
|
@ -58,6 +60,7 @@ mod cid;
|
||||||
mod cred;
|
mod cred;
|
||||||
mod datetime;
|
mod datetime;
|
||||||
pub mod eckey;
|
pub mod eckey;
|
||||||
|
pub mod image;
|
||||||
mod iname;
|
mod iname;
|
||||||
mod index;
|
mod index;
|
||||||
mod iutf8;
|
mod iutf8;
|
||||||
|
@ -83,6 +86,10 @@ pub type ValueSet = Box<dyn ValueSetT + Send + Sync + 'static>;
|
||||||
dyn_clone::clone_trait_object!(ValueSetT);
|
dyn_clone::clone_trait_object!(ValueSetT);
|
||||||
|
|
||||||
pub trait ValueSetT: std::fmt::Debug + DynClone {
|
pub trait ValueSetT: std::fmt::Debug + DynClone {
|
||||||
|
/// Returns whether the value was newly inserted. That is:
|
||||||
|
/// * If the set did not previously contain an equal value, true is returned.
|
||||||
|
/// * If the set already contained an equal value, false is returned, and the entry is not updated.
|
||||||
|
///
|
||||||
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError>;
|
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError>;
|
||||||
|
|
||||||
fn clear(&mut self);
|
fn clear(&mut self);
|
||||||
|
@ -562,6 +569,11 @@ pub trait ValueSetT: std::fmt::Debug + DynClone {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn as_imageset(&self) -> Option<&HashSet<ImageValue>> {
|
||||||
|
debug_assert!(false);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn repl_merge_valueset(
|
fn repl_merge_valueset(
|
||||||
&self,
|
&self,
|
||||||
_older: &ValueSet,
|
_older: &ValueSet,
|
||||||
|
@ -636,6 +648,7 @@ pub fn from_result_value_iter(
|
||||||
Value::UiHint(u) => ValueSetUiHint::new(u),
|
Value::UiHint(u) => ValueSetUiHint::new(u),
|
||||||
Value::AuditLogString(c, s) => ValueSetAuditLogString::new((c, s)),
|
Value::AuditLogString(c, s) => ValueSetAuditLogString::new((c, s)),
|
||||||
Value::EcKeyPrivate(k) => ValueSetEcKeyPrivate::new(&k),
|
Value::EcKeyPrivate(k) => ValueSetEcKeyPrivate::new(&k),
|
||||||
|
Value::Image(imagevalue) => image::ValueSetImage::new(imagevalue),
|
||||||
Value::PhoneNumber(_, _)
|
Value::PhoneNumber(_, _)
|
||||||
| Value::Passkey(_, _, _)
|
| Value::Passkey(_, _, _)
|
||||||
| Value::DeviceKey(_, _, _)
|
| Value::DeviceKey(_, _, _)
|
||||||
|
@ -702,6 +715,8 @@ pub fn from_value_iter(mut iter: impl Iterator<Item = Value>) -> Result<ValueSet
|
||||||
Value::TotpSecret(l, t) => ValueSetTotpSecret::new(l, t),
|
Value::TotpSecret(l, t) => ValueSetTotpSecret::new(l, t),
|
||||||
Value::AuditLogString(c, s) => ValueSetAuditLogString::new((c, s)),
|
Value::AuditLogString(c, s) => ValueSetAuditLogString::new((c, s)),
|
||||||
Value::EcKeyPrivate(k) => ValueSetEcKeyPrivate::new(&k),
|
Value::EcKeyPrivate(k) => ValueSetEcKeyPrivate::new(&k),
|
||||||
|
|
||||||
|
Value::Image(imagevalue) => image::ValueSetImage::new(imagevalue),
|
||||||
Value::PhoneNumber(_, _) => {
|
Value::PhoneNumber(_, _) => {
|
||||||
debug_assert!(false);
|
debug_assert!(false);
|
||||||
return Err(OperationError::InvalidValueState);
|
return Err(OperationError::InvalidValueState);
|
||||||
|
@ -757,6 +772,7 @@ pub fn from_db_valueset_v2(dbvs: DbValueSetV2) -> Result<ValueSet, OperationErro
|
||||||
debug_assert!(false);
|
debug_assert!(false);
|
||||||
Err(OperationError::InvalidValueState)
|
Err(OperationError::InvalidValueState)
|
||||||
}
|
}
|
||||||
|
DbValueSetV2::Image(set) => ValueSetImage::from_dbvs2(&set),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -801,5 +817,6 @@ pub fn from_repl_v1(rv1: &ReplAttrV1) -> Result<ValueSet, OperationError> {
|
||||||
ReplAttrV1::TotpSecret { set } => ValueSetTotpSecret::from_repl_v1(set),
|
ReplAttrV1::TotpSecret { set } => ValueSetTotpSecret::from_repl_v1(set),
|
||||||
ReplAttrV1::AuditLogString { map } => ValueSetAuditLogString::from_repl_v1(map),
|
ReplAttrV1::AuditLogString { map } => ValueSetAuditLogString::from_repl_v1(map),
|
||||||
ReplAttrV1::EcKeyPrivate { key } => ValueSetEcKeyPrivate::from_repl_v1(key),
|
ReplAttrV1::EcKeyPrivate { key } => ValueSetEcKeyPrivate::from_repl_v1(key),
|
||||||
|
ReplAttrV1::Image { set } => ValueSetImage::from_repl_v1(set),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ pub const NOT_ADMIN_TEST_PASSWORD: &str = "eicieY7ahchaoCh0eeTa";
|
||||||
pub static PORT_ALLOC: AtomicU16 = AtomicU16::new(18080);
|
pub static PORT_ALLOC: AtomicU16 = AtomicU16::new(18080);
|
||||||
|
|
||||||
pub use testkit_macros::test;
|
pub use testkit_macros::test;
|
||||||
|
use tracing::trace;
|
||||||
|
|
||||||
pub fn is_free_port(port: u16) -> bool {
|
pub fn is_free_port(port: u16) -> bool {
|
||||||
TcpStream::connect(("0.0.0.0", port)).is_err()
|
TcpStream::connect(("0.0.0.0", port)).is_err()
|
||||||
|
@ -325,7 +326,7 @@ pub async fn test_read_attrs(
|
||||||
let e = rset.first().expect("Failed to get first user from set");
|
let e = rset.first().expect("Failed to get first user from set");
|
||||||
|
|
||||||
for attr in attrs.iter() {
|
for attr in attrs.iter() {
|
||||||
println!("Reading {}", attr);
|
trace!("Reading {}", attr);
|
||||||
#[allow(clippy::unwrap_used)]
|
#[allow(clippy::unwrap_used)]
|
||||||
let is_ok = match *attr {
|
let is_ok = match *attr {
|
||||||
Attribute::RadiusSecret => rsclient
|
Attribute::RadiusSecret => rsclient
|
||||||
|
@ -335,7 +336,7 @@ pub async fn test_read_attrs(
|
||||||
.is_some(),
|
.is_some(),
|
||||||
_ => e.attrs.get(attr.as_ref()).is_some(),
|
_ => e.attrs.get(attr.as_ref()).is_some(),
|
||||||
};
|
};
|
||||||
dbg!(is_ok, is_readable);
|
trace!("is_ok: {}, is_readable: {}", is_ok, is_readable);
|
||||||
assert!(is_ok == is_readable)
|
assert!(is_ok == is_readable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
#![deny(warnings)]
|
#![deny(warnings)]
|
||||||
|
use std::path::Path;
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
use kanidm_proto::internal::ImageValue;
|
||||||
use kanidm_proto::v1::{
|
use kanidm_proto::v1::{
|
||||||
ApiToken, AuthCredential, AuthIssueSession, AuthMech, AuthRequest, AuthResponse, AuthState,
|
ApiToken, AuthCredential, AuthIssueSession, AuthMech, AuthRequest, AuthResponse, AuthState,
|
||||||
AuthStep, CURegState, CredentialDetailType, Entry, Filter, Modify, ModifyList, UatPurpose,
|
AuthStep, CURegState, CredentialDetailType, Entry, Filter, Modify, ModifyList, UatPurpose,
|
||||||
|
@ -11,7 +13,7 @@ use kanidmd_lib::prelude::{
|
||||||
Attribute, BUILTIN_GROUP_IDM_ADMINS_V1, BUILTIN_GROUP_SYSTEM_ADMINS_V1,
|
Attribute, BUILTIN_GROUP_IDM_ADMINS_V1, BUILTIN_GROUP_SYSTEM_ADMINS_V1,
|
||||||
IDM_PEOPLE_ACCOUNT_PASSWORD_IMPORT_PRIV_V1,
|
IDM_PEOPLE_ACCOUNT_PASSWORD_IMPORT_PRIV_V1,
|
||||||
};
|
};
|
||||||
use tracing::debug;
|
use tracing::{debug, trace};
|
||||||
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
@ -958,6 +960,74 @@ async fn test_server_rest_oauth2_basic_lifecycle(rsclient: KanidmClient) {
|
||||||
|
|
||||||
assert!(oauth2_config_updated2 != oauth2_config_updated3);
|
assert!(oauth2_config_updated2 != oauth2_config_updated3);
|
||||||
|
|
||||||
|
// Check we can upload an image
|
||||||
|
let image_path = Path::new("../../server/lib/src/valueset/image/test_images/ok.png");
|
||||||
|
assert!(image_path.exists());
|
||||||
|
let image_contents = std::fs::read(image_path).unwrap();
|
||||||
|
let image = ImageValue::new(
|
||||||
|
"test".to_string(),
|
||||||
|
kanidm_proto::internal::ImageType::Png,
|
||||||
|
image_contents,
|
||||||
|
);
|
||||||
|
|
||||||
|
let res = rsclient
|
||||||
|
.idm_oauth2_rs_update_image("test_integration", image)
|
||||||
|
.await;
|
||||||
|
trace!("update image result: {:?}", &res);
|
||||||
|
assert!(res.is_ok());
|
||||||
|
|
||||||
|
//test getting the image
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.get(rsclient.make_url("/ui/images/oauth2/test_integration"))
|
||||||
|
.bearer_auth(rsclient.get_token().await.unwrap());
|
||||||
|
|
||||||
|
let response = response
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|err| rsclient.handle_response_error(err))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
|
||||||
|
// check we can upload a *replacement* image
|
||||||
|
|
||||||
|
let image_path = Path::new("../../server/lib/src/valueset/image/test_images/ok.jpg");
|
||||||
|
trace!("image path {:?}", &image_path.canonicalize());
|
||||||
|
assert!(image_path.exists());
|
||||||
|
let jpg_file_contents = std::fs::read(image_path).unwrap();
|
||||||
|
let image = ImageValue::new(
|
||||||
|
"test".to_string(),
|
||||||
|
kanidm_proto::internal::ImageType::Jpg,
|
||||||
|
jpg_file_contents.clone(),
|
||||||
|
);
|
||||||
|
let res = rsclient
|
||||||
|
.idm_oauth2_rs_update_image("test_integration", image)
|
||||||
|
.await;
|
||||||
|
trace!("idm_oauth2_rs_update_image result: {:?}", &res);
|
||||||
|
assert!(res.is_ok());
|
||||||
|
|
||||||
|
// check it fails when we upload a jpg and say it's a webp
|
||||||
|
let image = ImageValue::new(
|
||||||
|
"test".to_string(),
|
||||||
|
kanidm_proto::internal::ImageType::Webp,
|
||||||
|
jpg_file_contents,
|
||||||
|
);
|
||||||
|
let res = rsclient
|
||||||
|
.idm_oauth2_rs_update_image("test_integration", image)
|
||||||
|
.await;
|
||||||
|
trace!("idm_oauth2_rs_update_image result: {:?}", &res);
|
||||||
|
assert!(res.is_err());
|
||||||
|
|
||||||
|
// check we can remove an image
|
||||||
|
|
||||||
|
let res = rsclient
|
||||||
|
.idm_oauth2_rs_delete_image("test_integration")
|
||||||
|
.await;
|
||||||
|
trace!("idm_oauth2_rs_delete_image result: {:?}", &res);
|
||||||
|
assert!(res.is_ok());
|
||||||
|
|
||||||
// Check we can delete a scope map.
|
// Check we can delete a scope map.
|
||||||
|
|
||||||
rsclient
|
rsclient
|
||||||
|
|
|
@ -9,7 +9,7 @@ pub struct Named {
|
||||||
|
|
||||||
#[derive(Debug, Args)]
|
#[derive(Debug, Args)]
|
||||||
pub struct DebugOpt {
|
pub struct DebugOpt {
|
||||||
/// Enable debbuging of the kanidm tool
|
/// Enable debugging of the kanidm tool
|
||||||
#[clap(short, long, env = "KANIDM_DEBUG")]
|
#[clap(short, long, env = "KANIDM_DEBUG")]
|
||||||
pub debug: bool,
|
pub debug: bool,
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ impl std::str::FromStr for OutputMode {
|
||||||
|
|
||||||
#[derive(Debug, Args, Clone)]
|
#[derive(Debug, Args, Clone)]
|
||||||
pub struct CommonOpt {
|
pub struct CommonOpt {
|
||||||
/// Enable debbuging of the kanidm tool
|
/// Enable debugging of the kanidm tool
|
||||||
#[clap(short, long, env = "KANIDM_DEBUG")]
|
#[clap(short, long, env = "KANIDM_DEBUG")]
|
||||||
pub debug: bool,
|
pub debug: bool,
|
||||||
/// The URL of the kanidm instance
|
/// The URL of the kanidm instance
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
|
|
||||||
|
|
||||||
use kanidm_proto::constants::DEFAULT_CLIENT_CONFIG_PATH;
|
use kanidm_proto::constants::DEFAULT_CLIENT_CONFIG_PATH;
|
||||||
pub const DEFAULT_IPA_CONFIG_PATH: &str = "/etc/kanidm/ipa-sync";
|
pub const DEFAULT_IPA_CONFIG_PATH: &str = "/etc/kanidm/ipa-sync";
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, clap::Parser)]
|
#[derive(Debug, clap::Parser)]
|
||||||
#[clap(about = "Kanidm FreeIPA Sync Driver")]
|
#[clap(about = "Kanidm FreeIPA Sync Driver")]
|
||||||
pub struct Opt {
|
pub struct Opt {
|
||||||
/// Enable debbuging of the sync driver
|
/// Enable debugging of the sync driver
|
||||||
#[clap(short, long, env = "KANIDM_DEBUG")]
|
#[clap(short, long, env = "KANIDM_DEBUG")]
|
||||||
pub debug: bool,
|
pub debug: bool,
|
||||||
/// Path to the client config file.
|
/// Path to the client config file.
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
|
|
||||||
use kanidm_proto::constants::DEFAULT_CLIENT_CONFIG_PATH;
|
use kanidm_proto::constants::DEFAULT_CLIENT_CONFIG_PATH;
|
||||||
pub const DEFAULT_LDAP_CONFIG_PATH: &str = "/etc/kanidm/ldap-sync";
|
pub const DEFAULT_LDAP_CONFIG_PATH: &str = "/etc/kanidm/ldap-sync";
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, clap::Parser)]
|
#[derive(Debug, clap::Parser)]
|
||||||
#[clap(about = "Kanidm LDAP Sync Driver")]
|
#[clap(about = "Kanidm LDAP Sync Driver")]
|
||||||
pub struct Opt {
|
pub struct Opt {
|
||||||
/// Enable debbuging of the sync driver
|
/// Enable debugging of the sync driver
|
||||||
#[clap(short, long, env = "KANIDM_DEBUG")]
|
#[clap(short, long, env = "KANIDM_DEBUG")]
|
||||||
pub debug: bool,
|
pub debug: bool,
|
||||||
/// Path to the client config file.
|
/// Path to the client config file.
|
||||||
|
|