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
This commit is contained in:
James Hodgkinson 2023-10-04 17:24:12 +10:00 committed by GitHub
parent d13a314a80
commit e7f594a1c1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
61 changed files with 2728 additions and 248 deletions

348
Cargo.lock generated
View file

@ -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",
] ]

View file

@ -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" }

View file

@ -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

View file

@ -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
===================== =====================

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
``` ```

View file

@ -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

View file

@ -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

View file

@ -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 }

View file

@ -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 => {}

View file

@ -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(),

View file

@ -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);

View file

@ -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";

View file

@ -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,
}
}
}

View file

@ -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,

View file

@ -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,

View file

@ -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(),
}; };

View file

@ -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)

View file

@ -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),

View file

@ -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(());
} }
} }

View file

@ -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]

View 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);

View file

@ -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
} }

View file

@ -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,

View file

@ -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,

View file

@ -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(),

View file

@ -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");

View file

@ -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

View file

@ -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

View file

@ -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);

View file

@ -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]

View file

@ -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.

View file

@ -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)]

View file

@ -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(

View file

@ -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

View file

@ -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 => {

View file

@ -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

View file

@ -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)

View file

@ -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
} }
} }

View 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());
}

View 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);
})
}

View 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;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View file

@ -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),
} }
} }

View file

@ -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)
} }
} }

View file

@ -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

View file

@ -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

View file

@ -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.

View 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.