mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-22 20:26:30 +01:00
[htmx] Admin ui for groups and users management (#3019)
Some checks are pending
Linting checks / clippy (push) Waiting to run
Linting checks / fmt (push) Waiting to run
Spell Check / codespell (push) Waiting to run
Container - Kanidm / Set image tag values (push) Waiting to run
Container - Kanidm / Build kanidm Docker image (push) Blocked by required conditions
Container - Kanidm / Push kanidm Docker image (push) Blocked by required conditions
Container - Kanidmd / Set image tag values (push) Waiting to run
Container - Kanidmd / Build kanidmd Docker image (push) Blocked by required conditions
Container - Kanidmd / Push kanidmd Docker image (push) Blocked by required conditions
Container - Radiusd / Set image tag values (push) Waiting to run
Container - Radiusd / Build radius Docker image (push) Blocked by required conditions
Container - Radiusd / Push radius Docker image (push) Blocked by required conditions
Javascript Linting / javascript_lint (push) Waiting to run
Javascript Linting / javascript_fmt (push) Waiting to run
GitHub Pages / pre_deploy (push) Waiting to run
GitHub Pages / fanout (${{ needs.pre_deploy.outputs.latest}}) (push) Blocked by required conditions
GitHub Pages / docs_master (push) Waiting to run
GitHub Pages / deploy (push) Blocked by required conditions
PyKanidm tests / tests (push) Waiting to run
Linux Build and Test / rust_build (push) Waiting to run
Linux Build and Test / rust_build_next (beta) (push) Waiting to run
Linux Build and Test / rust_build_next (nightly) (push) Waiting to run
Linux Build and Test / run_release (push) Waiting to run
Windows Build and Test / windows_build_kanidm (push) Waiting to run
Some checks are pending
Linting checks / clippy (push) Waiting to run
Linting checks / fmt (push) Waiting to run
Spell Check / codespell (push) Waiting to run
Container - Kanidm / Set image tag values (push) Waiting to run
Container - Kanidm / Build kanidm Docker image (push) Blocked by required conditions
Container - Kanidm / Push kanidm Docker image (push) Blocked by required conditions
Container - Kanidmd / Set image tag values (push) Waiting to run
Container - Kanidmd / Build kanidmd Docker image (push) Blocked by required conditions
Container - Kanidmd / Push kanidmd Docker image (push) Blocked by required conditions
Container - Radiusd / Set image tag values (push) Waiting to run
Container - Radiusd / Build radius Docker image (push) Blocked by required conditions
Container - Radiusd / Push radius Docker image (push) Blocked by required conditions
Javascript Linting / javascript_lint (push) Waiting to run
Javascript Linting / javascript_fmt (push) Waiting to run
GitHub Pages / pre_deploy (push) Waiting to run
GitHub Pages / fanout (${{ needs.pre_deploy.outputs.latest}}) (push) Blocked by required conditions
GitHub Pages / docs_master (push) Waiting to run
GitHub Pages / deploy (push) Blocked by required conditions
PyKanidm tests / tests (push) Waiting to run
Linux Build and Test / rust_build (push) Waiting to run
Linux Build and Test / rust_build_next (beta) (push) Waiting to run
Linux Build and Test / rust_build_next (nightly) (push) Waiting to run
Linux Build and Test / run_release (push) Waiting to run
Windows Build and Test / windows_build_kanidm (push) Waiting to run
* Some progress on admin ui for managing groups and users * Improve scim querying --------- Co-authored-by: William Brown <william@blackhats.net.au>
This commit is contained in:
parent
9611a7f976
commit
857dcf5087
159
Cargo.lock
generated
159
Cargo.lock
generated
|
@ -113,9 +113,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.95"
|
||||
version = "1.0.96"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
|
||||
checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4"
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
|
@ -665,9 +665,9 @@ checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9"
|
|||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.13"
|
||||
version = "1.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7777341816418c02e033934a09f20dc0ccaf65a5201ef8a450ae0105a573fda"
|
||||
checksum = "c736e259eea577f443d5c86c304f9f4ae0295c43f3ba05c21f1d66b5f06001af"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
|
@ -727,9 +727,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.28"
|
||||
version = "4.5.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e77c3243bd94243c03672cb5154667347c457ca271254724f9f393aee1c05ff"
|
||||
checksum = "92b7b18d71fad5313a1e320fa9897994228ce274b60faa4d694fe0ea89cd9e6d"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
|
@ -737,9 +737,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.27"
|
||||
version = "4.5.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7"
|
||||
checksum = "a35db2071778a7344791a4fb4f95308b5673d219dee3ae348b86642574ecc90c"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
|
@ -749,9 +749,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "clap_complete"
|
||||
version = "4.5.44"
|
||||
version = "4.5.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "375f9d8255adeeedd51053574fd8d4ba875ea5fa558e86617b07f09f1680c8b6"
|
||||
checksum = "1e3040c8291884ddf39445dc033c70abc2bc44a42f0a3a00571a0f483a83f0cd"
|
||||
dependencies = [
|
||||
"clap",
|
||||
]
|
||||
|
@ -812,16 +812,16 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "concread"
|
||||
version = "0.5.3"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cba00cef522c2597dfbb0a8d1b0ac8ac2b99714f50cc354cda71da63164da0be"
|
||||
checksum = "0a06c26e76cd1d7a88a44324d0cf18b11589be552e97af09bee345f7e7334c6d"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"arc-swap",
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-queue",
|
||||
"crossbeam-utils",
|
||||
"lru",
|
||||
"lru 0.13.0",
|
||||
"smallvec",
|
||||
"sptr",
|
||||
"tokio",
|
||||
|
@ -1021,9 +1021,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "csv-core"
|
||||
version = "0.1.11"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70"
|
||||
checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
@ -1126,9 +1126,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.7.0"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e60eed09d8c01d3cee5b7d30acb059b76614c918fa0f992e0dd6eeb10daad6f"
|
||||
checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010"
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
|
@ -1292,9 +1292,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "document-features"
|
||||
version = "0.2.10"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0"
|
||||
checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d"
|
||||
dependencies = [
|
||||
"litrs",
|
||||
]
|
||||
|
@ -1383,9 +1383,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.1"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
|
@ -2240,9 +2240,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.7"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e"
|
||||
checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
|
@ -2443,7 +2443,7 @@ dependencies = [
|
|||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"h2 0.4.7",
|
||||
"h2 0.4.8",
|
||||
"http 1.2.0",
|
||||
"http-body 1.0.1",
|
||||
"httparse",
|
||||
|
@ -2801,6 +2801,15 @@ dependencies = [
|
|||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.14"
|
||||
|
@ -3085,7 +3094,7 @@ dependencies = [
|
|||
"kanidmd_core",
|
||||
"kanidmd_testkit",
|
||||
"libc",
|
||||
"lru",
|
||||
"lru 0.12.5",
|
||||
"mimalloc",
|
||||
"notify-debouncer-full",
|
||||
"prctl",
|
||||
|
@ -3467,9 +3476,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.25"
|
||||
version = "0.4.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
|
||||
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
|
||||
|
||||
[[package]]
|
||||
name = "lru"
|
||||
|
@ -3480,6 +3489,15 @@ dependencies = [
|
|||
"hashbrown 0.15.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "227748d55f2f0ab4735d87fd623798cb6b664512fe979705f829c9f81c934465"
|
||||
dependencies = [
|
||||
"hashbrown 0.15.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "malloced"
|
||||
version = "1.3.1"
|
||||
|
@ -3567,9 +3585,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
|||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.3"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924"
|
||||
checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
]
|
||||
|
@ -3622,9 +3640,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.13"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0dab59f8e050d5df8e4dd87d9206fb6f65a483e20ac9fda365ade4fab353196c"
|
||||
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
|
@ -3933,9 +3951,9 @@ checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
|
|||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.70"
|
||||
version = "0.10.71"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6"
|
||||
checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"cfg-if",
|
||||
|
@ -3965,9 +3983,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
|||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.105"
|
||||
version = "0.9.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b22d5b84be05a8d6947c7cb71f7c849aa0f112acd4bf51c2a7c1c988ac0a9dc"
|
||||
checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
|
@ -4367,9 +4385,9 @@ checksum = "744a264d26b88a6a7e37cbad97953fa233b94d585236310bcbc88474b4092d79"
|
|||
|
||||
[[package]]
|
||||
name = "prost"
|
||||
version = "0.13.4"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c0fef6c4230e4ccf618a35c59d7ede15dea37de8427500f50aff708806e42ec"
|
||||
checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"prost-derive",
|
||||
|
@ -4377,12 +4395,12 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "prost-derive"
|
||||
version = "0.13.4"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3"
|
||||
checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.13.0",
|
||||
"itertools 0.14.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
|
@ -4460,9 +4478,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.5.9"
|
||||
version = "0.5.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c40286217b4ba3a71d644d752e6a0b71f13f1b6a2c5311acfcbe0c2418ed904"
|
||||
checksum = "e46f3055866785f6b92bc6164b76be02ca8f2eb4b002c0354b28cf4c119e5944"
|
||||
dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
|
@ -4513,9 +4531,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.8"
|
||||
version = "0.5.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
|
||||
checksum = "82b568323e98e49e2a0899dcee453dd679fae22d69adf9b11dd508d1549b7e2f"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
]
|
||||
|
@ -4665,7 +4683,7 @@ dependencies = [
|
|||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2 0.4.7",
|
||||
"h2 0.4.8",
|
||||
"http 1.2.0",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
|
@ -4713,15 +4731,14 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.8"
|
||||
version = "0.17.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
|
||||
checksum = "d34b5020fcdea098ef7d95e9f89ec15952123a4a039badd09fabebe9e963e839"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"getrandom 0.2.15",
|
||||
"libc",
|
||||
"spin",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
@ -4832,9 +4849,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.22"
|
||||
version = "0.23.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fb9263ab4eb695e42321db096e3b8fbd715a59b154d5c88d82db2175b681ba7"
|
||||
checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"ring",
|
||||
|
@ -5017,9 +5034,9 @@ checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03"
|
|||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.217"
|
||||
version = "1.0.218"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
|
||||
checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
@ -5055,9 +5072,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.217"
|
||||
version = "1.0.218"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
|
||||
checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -5066,9 +5083,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.138"
|
||||
version = "1.0.139"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949"
|
||||
checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
|
@ -5224,9 +5241,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.13.2"
|
||||
version = "1.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||
checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
@ -5427,9 +5444,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
|||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.16.0"
|
||||
version = "3.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91"
|
||||
checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"fastrand",
|
||||
|
@ -5564,9 +5581,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
|||
|
||||
[[package]]
|
||||
name = "tls_codec"
|
||||
version = "0.4.1"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5e78c9c330f8c85b2bae7c8368f2739157db9991235123aa1b15ef9502bfb6a"
|
||||
checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b"
|
||||
dependencies = [
|
||||
"tls_codec_derive",
|
||||
"zeroize",
|
||||
|
@ -5574,9 +5591,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tls_codec_derive"
|
||||
version = "0.4.1"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d9ef545650e79f30233c0003bcc2504d7efac6dad25fca40744de773fe2049c"
|
||||
checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -5705,7 +5722,7 @@ dependencies = [
|
|||
"axum",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"h2 0.4.7",
|
||||
"h2 0.4.8",
|
||||
"http 1.2.0",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
|
@ -5939,9 +5956,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.17.0"
|
||||
version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
|
@ -5957,9 +5974,9 @@ checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217"
|
|||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.16"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034"
|
||||
checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
|
@ -6069,9 +6086,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.13.1"
|
||||
version = "1.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0"
|
||||
checksum = "93d59ca99a559661b96bf898d8fce28ed87935fd2bea9f05983c1464dd6c71b1"
|
||||
dependencies = [
|
||||
"getrandom 0.3.1",
|
||||
"serde",
|
||||
|
|
|
@ -631,6 +631,71 @@ impl From<Attribute> for String {
|
|||
}
|
||||
}
|
||||
|
||||
/// Sub attributes are a component of SCIM, allowing tagged sub properties of a complex
|
||||
/// attribute to be accessed.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)]
|
||||
#[serde(rename_all = "lowercase", try_from = "&str", into = "AttrString")]
|
||||
pub enum SubAttribute {
|
||||
/// Denotes a primary value.
|
||||
Primary,
|
||||
|
||||
#[cfg(not(test))]
|
||||
Custom(AttrString),
|
||||
}
|
||||
|
||||
impl From<SubAttribute> for AttrString {
|
||||
fn from(val: SubAttribute) -> Self {
|
||||
AttrString::from(val.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for SubAttribute {
|
||||
fn from(value: &str) -> Self {
|
||||
Self::inner_from_str(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for SubAttribute {
|
||||
type Err = Infallible;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
Ok(Self::inner_from_str(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl SubAttribute {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
SubAttribute::Primary => SUB_ATTR_PRIMARY,
|
||||
#[cfg(not(test))]
|
||||
SubAttribute::Custom(s) => s,
|
||||
}
|
||||
}
|
||||
|
||||
// We allow this because the standard lib from_str is fallible, and we want an infallible version.
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
fn inner_from_str(value: &str) -> Self {
|
||||
// Could this be something like heapless to save allocations? Also gives a way
|
||||
// to limit length of str?
|
||||
match value.to_lowercase().as_str() {
|
||||
SUB_ATTR_PRIMARY => SubAttribute::Primary,
|
||||
|
||||
#[cfg(not(test))]
|
||||
_ => SubAttribute::Custom(AttrString::from(value)),
|
||||
|
||||
// Allowed only in tests
|
||||
#[allow(clippy::unreachable)]
|
||||
#[cfg(test)]
|
||||
_ => {
|
||||
unreachable!(
|
||||
"Check that you've implemented the SubAttribute conversion for {:?}",
|
||||
value
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::Attribute;
|
||||
|
|
|
@ -220,6 +220,8 @@ pub const ATTR_VERSION: &str = "version";
|
|||
pub const ATTR_WEBAUTHN_ATTESTATION_CA_LIST: &str = "webauthn_attestation_ca_list";
|
||||
pub const ATTR_ALLOW_PRIMARY_CRED_FALLBACK: &str = "allow_primary_cred_fallback";
|
||||
|
||||
pub const SUB_ATTR_PRIMARY: &str = "primary";
|
||||
|
||||
pub const OAUTH2_SCOPE_EMAIL: &str = ATTR_EMAIL;
|
||||
pub const OAUTH2_SCOPE_GROUPS: &str = "groups";
|
||||
pub const OAUTH2_SCOPE_SSH_PUBLICKEYS: &str = "ssh_publickeys";
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
//! These are types that a client will send to the server.
|
||||
use super::ScimEntryGetQuery;
|
||||
use super::ScimOauth2ClaimMapJoinChar;
|
||||
use crate::attribute::Attribute;
|
||||
use crate::attribute::{Attribute, SubAttribute};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value as JsonValue;
|
||||
use serde_with::formats::PreferMany;
|
||||
|
@ -134,3 +134,59 @@ impl TryFrom<ScimEntryPutKanidm> for ScimEntryPutGeneric {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
pub struct AttrPath {
|
||||
pub a: Attribute,
|
||||
pub s: Option<SubAttribute>,
|
||||
}
|
||||
|
||||
impl From<Attribute> for AttrPath {
|
||||
fn from(a: Attribute) -> Self {
|
||||
Self { a, s: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(Attribute, SubAttribute)> for AttrPath {
|
||||
fn from((a, s): (Attribute, SubAttribute)) -> Self {
|
||||
Self { a, s: Some(s) }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
pub enum ScimFilter {
|
||||
Or(Box<ScimFilter>, Box<ScimFilter>),
|
||||
And(Box<ScimFilter>, Box<ScimFilter>),
|
||||
Not(Box<ScimFilter>),
|
||||
|
||||
Present(AttrPath),
|
||||
Equal(AttrPath, JsonValue),
|
||||
NotEqual(AttrPath, JsonValue),
|
||||
Contains(AttrPath, JsonValue),
|
||||
StartsWith(AttrPath, JsonValue),
|
||||
EndsWith(AttrPath, JsonValue),
|
||||
Greater(AttrPath, JsonValue),
|
||||
Less(AttrPath, JsonValue),
|
||||
GreaterOrEqual(AttrPath, JsonValue),
|
||||
LessOrEqual(AttrPath, JsonValue),
|
||||
|
||||
Complex(Attribute, Box<ScimComplexFilter>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
pub enum ScimComplexFilter {
|
||||
Or(Box<ScimComplexFilter>, Box<ScimComplexFilter>),
|
||||
And(Box<ScimComplexFilter>, Box<ScimComplexFilter>),
|
||||
Not(Box<ScimComplexFilter>),
|
||||
|
||||
Present(SubAttribute),
|
||||
Equal(SubAttribute, JsonValue),
|
||||
NotEqual(SubAttribute, JsonValue),
|
||||
Contains(SubAttribute, JsonValue),
|
||||
StartsWith(SubAttribute, JsonValue),
|
||||
EndsWith(SubAttribute, JsonValue),
|
||||
Greater(SubAttribute, JsonValue),
|
||||
Less(SubAttribute, JsonValue),
|
||||
GreaterOrEqual(SubAttribute, JsonValue),
|
||||
LessOrEqual(SubAttribute, JsonValue),
|
||||
}
|
||||
|
|
|
@ -257,6 +257,98 @@ pub enum ScimValueKanidm {
|
|||
UiHints(Vec<UiHint>),
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Serialize, Debug, Clone, ToSchema)]
|
||||
pub struct ScimPerson {
|
||||
pub uuid: Uuid,
|
||||
pub name: String,
|
||||
pub displayname: String,
|
||||
pub spn: String,
|
||||
pub description: Option<String>,
|
||||
pub mails: Vec<ScimMail>,
|
||||
pub managed_by: Option<ScimReference>,
|
||||
pub groups: Vec<ScimReference>,
|
||||
}
|
||||
|
||||
impl TryFrom<ScimEntryKanidm> for ScimPerson {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(scim_entry: ScimEntryKanidm) -> Result<Self, Self::Error> {
|
||||
let uuid = scim_entry.header.id;
|
||||
let name = scim_entry
|
||||
.attrs
|
||||
.get(&Attribute::Name)
|
||||
.and_then(|v| match v {
|
||||
ScimValueKanidm::String(s) => Some(s.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.ok_or(())?;
|
||||
|
||||
let displayname = scim_entry
|
||||
.attrs
|
||||
.get(&Attribute::DisplayName)
|
||||
.and_then(|v| match v {
|
||||
ScimValueKanidm::String(s) => Some(s.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.ok_or(())?;
|
||||
|
||||
let spn = scim_entry
|
||||
.attrs
|
||||
.get(&Attribute::Spn)
|
||||
.and_then(|v| match v {
|
||||
ScimValueKanidm::String(s) => Some(s.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.ok_or(())?;
|
||||
|
||||
let description = scim_entry
|
||||
.attrs
|
||||
.get(&Attribute::Description)
|
||||
.and_then(|v| match v {
|
||||
ScimValueKanidm::String(s) => Some(s.clone()),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
let mails = scim_entry
|
||||
.attrs
|
||||
.get(&Attribute::Mail)
|
||||
.and_then(|v| match v {
|
||||
ScimValueKanidm::Mail(m) => Some(m.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let groups = scim_entry
|
||||
.attrs
|
||||
.get(&Attribute::DirectMemberOf)
|
||||
.and_then(|v| match v {
|
||||
ScimValueKanidm::EntryReferences(v) => Some(v.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let managed_by = scim_entry
|
||||
.attrs
|
||||
.get(&Attribute::EntryManagedBy)
|
||||
.and_then(|v| match v {
|
||||
ScimValueKanidm::EntryReference(v) => Some(v.clone()),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
Ok(ScimPerson {
|
||||
uuid,
|
||||
name,
|
||||
displayname,
|
||||
spn,
|
||||
description,
|
||||
mails,
|
||||
managed_by,
|
||||
groups,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for ScimValueKanidm {
|
||||
fn from(b: bool) -> Self {
|
||||
Self::Bool(b)
|
||||
|
|
|
@ -19,7 +19,7 @@ pub use self::auth::*;
|
|||
pub use self::unix::*;
|
||||
|
||||
/// The type of Account in use.
|
||||
#[derive(Clone, Copy, Debug, ToSchema)]
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, ToSchema)]
|
||||
pub enum AccountType {
|
||||
Person,
|
||||
ServiceAccount,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use super::{QueryServerReadV1, QueryServerWriteV1};
|
||||
use kanidm_proto::scim_v1::{
|
||||
server::ScimEntryKanidm, ScimEntryGetQuery, ScimSyncRequest, ScimSyncState,
|
||||
client::ScimFilter, server::ScimEntryKanidm, ScimEntryGetQuery, ScimSyncRequest, ScimSyncState,
|
||||
};
|
||||
use kanidmd_lib::idm::scim::{
|
||||
GenerateScimSyncTokenEvent, ScimSyncFinaliseEvent, ScimSyncTerminateEvent, ScimSyncUpdateEvent,
|
||||
|
@ -229,4 +229,27 @@ impl QueryServerReadV1 {
|
|||
.qs_read
|
||||
.scim_entry_id_get_ext(target_uuid, class, query, ident)
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "info",
|
||||
skip_all,
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
pub async fn scim_entry_search(
|
||||
&self,
|
||||
client_auth_info: ClientAuthInfo,
|
||||
eventid: Uuid,
|
||||
filter: ScimFilter,
|
||||
query: ScimEntryGetQuery,
|
||||
) -> Result<Vec<ScimEntryKanidm>, OperationError> {
|
||||
let ct = duration_from_epoch_now();
|
||||
let mut idms_prox_read = self.idms.proxy_read().await?;
|
||||
let ident = idms_prox_read
|
||||
.validate_client_auth_info_to_ident(client_auth_info, ct)
|
||||
.inspect_err(|err| {
|
||||
error!(?err, "Invalid identity");
|
||||
})?;
|
||||
|
||||
idms_prox_read.qs_read.scim_search_ext(ident, filter, query)
|
||||
}
|
||||
}
|
||||
|
|
19
server/core/src/https/views/admin/mod.rs
Normal file
19
server/core/src/https/views/admin/mod.rs
Normal file
|
@ -0,0 +1,19 @@
|
|||
use crate::https::ServerState;
|
||||
use axum::routing::get;
|
||||
use axum::Router;
|
||||
use axum_htmx::HxRequestGuardLayer;
|
||||
|
||||
mod persons;
|
||||
|
||||
pub fn admin_router() -> Router<ServerState> {
|
||||
let unguarded_router = Router::new()
|
||||
.route("/persons", get(persons::view_persons_get))
|
||||
.route(
|
||||
"/person/:person_uuid/view",
|
||||
get(persons::view_person_view_get),
|
||||
);
|
||||
|
||||
let guarded_router = Router::new().layer(HxRequestGuardLayer::new("/ui"));
|
||||
|
||||
Router::new().merge(unguarded_router).merge(guarded_router)
|
||||
}
|
193
server/core/src/https/views/admin/persons.rs
Normal file
193
server/core/src/https/views/admin/persons.rs
Normal file
|
@ -0,0 +1,193 @@
|
|||
use crate::https::extractors::{DomainInfo, VerifiedClientInformation};
|
||||
use crate::https::middleware::KOpId;
|
||||
use crate::https::views::errors::HtmxError;
|
||||
use crate::https::views::navbar::NavbarCtx;
|
||||
use crate::https::views::Urls;
|
||||
use crate::https::ServerState;
|
||||
use askama::Template;
|
||||
use axum::extract::{Path, State};
|
||||
use axum::http::Uri;
|
||||
use axum::response::{ErrorResponse, IntoResponse, Response};
|
||||
use axum::Extension;
|
||||
use axum_htmx::{HxPushUrl, HxRequest};
|
||||
use futures_util::TryFutureExt;
|
||||
use kanidm_proto::attribute::Attribute;
|
||||
use kanidm_proto::internal::OperationError;
|
||||
use kanidm_proto::scim_v1::client::ScimFilter;
|
||||
use kanidm_proto::scim_v1::server::{ScimEffectiveAccess, ScimEntryKanidm, ScimPerson};
|
||||
use kanidm_proto::scim_v1::ScimEntryGetQuery;
|
||||
use kanidmd_lib::constants::EntryClass;
|
||||
use kanidmd_lib::idm::server::DomainInfoRead;
|
||||
use kanidmd_lib::idm::ClientAuthInfo;
|
||||
use std::str::FromStr;
|
||||
use uuid::Uuid;
|
||||
|
||||
const PERSON_ATTRIBUTES: [Attribute; 9] = [
|
||||
Attribute::Uuid,
|
||||
Attribute::Description,
|
||||
Attribute::Name,
|
||||
Attribute::DisplayName,
|
||||
Attribute::Spn,
|
||||
Attribute::Mail,
|
||||
Attribute::Class,
|
||||
Attribute::EntryManagedBy,
|
||||
Attribute::DirectMemberOf,
|
||||
];
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "admin/admin_panel_template.html")]
|
||||
pub(crate) struct PersonsView {
|
||||
navbar_ctx: NavbarCtx,
|
||||
partial: PersonsPartialView,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "admin/admin_persons_partial.html")]
|
||||
struct PersonsPartialView {
|
||||
persons: Vec<(ScimPerson, ScimEffectiveAccess)>,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "admin/admin_panel_template.html")]
|
||||
struct PersonView {
|
||||
partial: PersonViewPartial,
|
||||
navbar_ctx: NavbarCtx,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "admin/admin_person_view_partial.html")]
|
||||
struct PersonViewPartial {
|
||||
person: ScimPerson,
|
||||
scim_effective_access: ScimEffectiveAccess,
|
||||
}
|
||||
|
||||
pub(crate) async fn view_person_view_get(
|
||||
State(state): State<ServerState>,
|
||||
HxRequest(is_htmx): HxRequest,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
|
||||
Path(uuid): Path<Uuid>,
|
||||
DomainInfo(domain_info): DomainInfo,
|
||||
) -> axum::response::Result<Response> {
|
||||
let (person, scim_effective_access) =
|
||||
get_person_info(uuid, state, &kopid, client_auth_info, domain_info.clone()).await?;
|
||||
let person_partial = PersonViewPartial {
|
||||
person,
|
||||
scim_effective_access,
|
||||
};
|
||||
|
||||
let path_string = format!("/ui/admin/person/{uuid}/view");
|
||||
let uri = Uri::from_str(path_string.as_str())
|
||||
.map_err(|_| HtmxError::new(&kopid, OperationError::Backend, domain_info.clone()))?;
|
||||
let push_url = HxPushUrl(uri);
|
||||
Ok(if is_htmx {
|
||||
(push_url, person_partial).into_response()
|
||||
} else {
|
||||
(
|
||||
push_url,
|
||||
PersonView {
|
||||
partial: person_partial,
|
||||
navbar_ctx: NavbarCtx { domain_info },
|
||||
},
|
||||
)
|
||||
.into_response()
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn view_persons_get(
|
||||
State(state): State<ServerState>,
|
||||
HxRequest(is_htmx): HxRequest,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
DomainInfo(domain_info): DomainInfo,
|
||||
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
|
||||
) -> axum::response::Result<Response> {
|
||||
let persons = get_persons_info(state, &kopid, client_auth_info, domain_info.clone()).await?;
|
||||
let persons_partial = PersonsPartialView { persons };
|
||||
|
||||
let push_url = HxPushUrl(Uri::from_static("/ui/admin/persons"));
|
||||
Ok(if is_htmx {
|
||||
(push_url, persons_partial).into_response()
|
||||
} else {
|
||||
(
|
||||
push_url,
|
||||
PersonsView {
|
||||
navbar_ctx: NavbarCtx { domain_info },
|
||||
partial: persons_partial,
|
||||
},
|
||||
)
|
||||
.into_response()
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_person_info(
|
||||
uuid: Uuid,
|
||||
state: ServerState,
|
||||
kopid: &KOpId,
|
||||
client_auth_info: ClientAuthInfo,
|
||||
domain_info: DomainInfoRead,
|
||||
) -> Result<(ScimPerson, ScimEffectiveAccess), ErrorResponse> {
|
||||
let scim_entry: ScimEntryKanidm = state
|
||||
.qe_r_ref
|
||||
.scim_entry_id_get(
|
||||
client_auth_info.clone(),
|
||||
kopid.eventid,
|
||||
uuid.to_string(),
|
||||
EntryClass::Person,
|
||||
ScimEntryGetQuery {
|
||||
attributes: Some(Vec::from(PERSON_ATTRIBUTES)),
|
||||
ext_access_check: true,
|
||||
},
|
||||
)
|
||||
.map_err(|op_err| HtmxError::new(kopid, op_err, domain_info.clone()))
|
||||
.await?;
|
||||
|
||||
if let Some(personinfo_info) = scimentry_into_personinfo(scim_entry) {
|
||||
Ok(personinfo_info)
|
||||
} else {
|
||||
Err(HtmxError::new(kopid, OperationError::InvalidState, domain_info.clone()).into())
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_persons_info(
|
||||
state: ServerState,
|
||||
kopid: &KOpId,
|
||||
client_auth_info: ClientAuthInfo,
|
||||
domain_info: DomainInfoRead,
|
||||
) -> Result<Vec<(ScimPerson, ScimEffectiveAccess)>, ErrorResponse> {
|
||||
let filter = ScimFilter::Equal(Attribute::Class.into(), EntryClass::Person.into());
|
||||
|
||||
let base: Vec<ScimEntryKanidm> = state
|
||||
.qe_r_ref
|
||||
.scim_entry_search(
|
||||
client_auth_info.clone(),
|
||||
kopid.eventid,
|
||||
filter,
|
||||
ScimEntryGetQuery {
|
||||
attributes: Some(Vec::from(PERSON_ATTRIBUTES)),
|
||||
ext_access_check: true,
|
||||
},
|
||||
)
|
||||
.map_err(|op_err| HtmxError::new(kopid, op_err, domain_info.clone()))
|
||||
.await?;
|
||||
|
||||
// TODO: inefficient to sort here
|
||||
let mut persons: Vec<_> = base
|
||||
.into_iter()
|
||||
// TODO: Filtering away unsuccessful entries may not be desired.
|
||||
.filter_map(scimentry_into_personinfo)
|
||||
.collect();
|
||||
|
||||
persons.sort_by_key(|(sp, _)| sp.uuid);
|
||||
persons.reverse();
|
||||
|
||||
Ok(persons)
|
||||
}
|
||||
|
||||
fn scimentry_into_personinfo(
|
||||
scim_entry: ScimEntryKanidm,
|
||||
) -> Option<(ScimPerson, ScimEffectiveAccess)> {
|
||||
let scim_effective_access = scim_entry.ext_access_check.clone()?; // TODO: This should be an error msg.
|
||||
let person = ScimPerson::try_from(scim_entry).ok()?;
|
||||
|
||||
Some((person, scim_effective_access))
|
||||
}
|
|
@ -45,14 +45,14 @@ pub(crate) async fn view_apps_get(
|
|||
.await
|
||||
.map_err(|old| HtmxError::new(&kopid, old, domain_info.clone()))?;
|
||||
|
||||
let apps_partial = AppsPartialView { apps: app_links };
|
||||
|
||||
Ok({
|
||||
(
|
||||
HxPushUrl(Uri::from_static(Urls::Apps.as_ref())),
|
||||
AppsView {
|
||||
navbar_ctx: NavbarCtx { domain_info },
|
||||
apps_partial: AppsPartialView { apps: app_links },
|
||||
},
|
||||
)
|
||||
.into_response()
|
||||
let apps_view = AppsView {
|
||||
navbar_ctx: NavbarCtx { domain_info },
|
||||
|
||||
apps_partial,
|
||||
};
|
||||
(HxPushUrl(Uri::from_static(Urls::Apps.as_ref())), apps_view).into_response()
|
||||
})
|
||||
}
|
||||
|
|
|
@ -105,6 +105,7 @@ pub(crate) async fn view_enrol_get(
|
|||
|
||||
Ok(ProfileView {
|
||||
navbar_ctx: NavbarCtx { domain_info },
|
||||
|
||||
profile_partial: EnrolDeviceView {
|
||||
menu_active_item: ProfileMenuItems::EnrolDevice,
|
||||
qr_code_svg,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Redirect, Response};
|
||||
use axum_htmx::{HxReswap, HxRetarget, SwapOption};
|
||||
use axum_htmx::{HxEvent, HxResponseTrigger, HxReswap, HxRetarget, SwapOption};
|
||||
use kanidmd_lib::idm::server::DomainInfoRead;
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
|
@ -8,7 +8,7 @@ use uuid::Uuid;
|
|||
use kanidm_proto::internal::OperationError;
|
||||
|
||||
use crate::https::middleware::KOpId;
|
||||
use crate::https::views::UnrecoverableErrorView;
|
||||
use crate::https::views::{ErrorToastPartial, UnrecoverableErrorView};
|
||||
// #[derive(Template)]
|
||||
// #[template(path = "recoverable_error_partial.html")]
|
||||
// struct ErrorPartialView {
|
||||
|
@ -41,7 +41,23 @@ impl IntoResponse for HtmxError {
|
|||
| OperationError::SessionExpired
|
||||
| OperationError::InvalidSessionState => Redirect::to("/ui").into_response(),
|
||||
OperationError::SystemProtectedObject | OperationError::AccessDenied => {
|
||||
(StatusCode::FORBIDDEN, body).into_response()
|
||||
let trigger = HxResponseTrigger::after_swap([HxEvent::new(
|
||||
"permissionDenied".to_string(),
|
||||
)]);
|
||||
(
|
||||
trigger,
|
||||
HxRetarget("main".to_string()),
|
||||
HxReswap(SwapOption::BeforeEnd),
|
||||
(
|
||||
StatusCode::FORBIDDEN,
|
||||
ErrorToastPartial {
|
||||
err_code: inner,
|
||||
operation_id: kopid,
|
||||
},
|
||||
)
|
||||
.into_response(),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
OperationError::NoMatchingEntries => {
|
||||
(StatusCode::NOT_FOUND, body).into_response()
|
||||
|
|
|
@ -8,6 +8,7 @@ use axum::{
|
|||
|
||||
use axum_htmx::HxRequestGuardLayer;
|
||||
|
||||
use crate::https::views::admin::admin_router;
|
||||
use constants::Urls;
|
||||
use kanidmd_lib::{
|
||||
idm::server::DomainInfoRead,
|
||||
|
@ -16,6 +17,7 @@ use kanidmd_lib::{
|
|||
|
||||
use crate::https::ServerState;
|
||||
|
||||
mod admin;
|
||||
mod apps;
|
||||
pub(crate) mod constants;
|
||||
mod cookies;
|
||||
|
@ -36,6 +38,13 @@ struct UnrecoverableErrorView {
|
|||
domain_info: DomainInfoRead,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "admin/error_toast.html")]
|
||||
struct ErrorToastPartial {
|
||||
err_code: OperationError,
|
||||
operation_id: Uuid,
|
||||
}
|
||||
|
||||
pub fn view_router() -> Router<ServerState> {
|
||||
let mut unguarded_router = Router::new()
|
||||
.route(
|
||||
|
@ -122,7 +131,11 @@ pub fn view_router() -> Router<ServerState> {
|
|||
.route("/api/cu_commit", post(reset::commit))
|
||||
.layer(HxRequestGuardLayer::new("/ui"));
|
||||
|
||||
Router::new().merge(unguarded_router).merge(guarded_router)
|
||||
let admin_router = admin_router();
|
||||
Router::new()
|
||||
.merge(unguarded_router)
|
||||
.merge(guarded_router)
|
||||
.nest("/admin", admin_router)
|
||||
}
|
||||
|
||||
/// Serde deserialization decorator to map empty Strings to None,
|
||||
|
|
|
@ -48,6 +48,7 @@ pub(crate) async fn view_profile_get(
|
|||
|
||||
Ok(ProfileView {
|
||||
navbar_ctx: NavbarCtx { domain_info },
|
||||
|
||||
profile_partial: ProfilePartialView {
|
||||
menu_active_item: ProfileMenuItems::UserProfile,
|
||||
can_rw,
|
||||
|
|
|
@ -20,6 +20,15 @@ body {
|
|||
max-width: 680px;
|
||||
}
|
||||
|
||||
/*
|
||||
* Bootstrap 5.3 fix for input-group validation
|
||||
* :has checks that a child can be selected with the selector
|
||||
* + selects the next sibling.
|
||||
*/
|
||||
.was-validated .input-group:has(.form-control:invalid) + .invalid-feedback {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/*
|
||||
* Sidebar
|
||||
*/
|
||||
|
|
10
server/core/templates/admin/admin_panel_template.html
Normal file
10
server/core/templates/admin/admin_panel_template.html
Normal file
|
@ -0,0 +1,10 @@
|
|||
(% extends "base_htmx_with_nav.html" %)
|
||||
|
||||
(% block title %)Admin Panel(% endblock %)
|
||||
|
||||
(% block head %)
|
||||
(% endblock %)
|
||||
|
||||
(% block main %)
|
||||
(( partial|safe ))
|
||||
(% endblock %)
|
19
server/core/templates/admin/admin_partial_base.html
Normal file
19
server/core/templates/admin/admin_partial_base.html
Normal file
|
@ -0,0 +1,19 @@
|
|||
<main class="container-xxl pb-5">
|
||||
<div class="d-flex flex-sm-row flex-column">
|
||||
<div class="list-group side-menu">
|
||||
<a href="/ui/admin/persons" hx-target="#main" class="list-group-item list-group-item-action (% block persons_item_extra_classes%)(%endblock%)">
|
||||
<img src="/pkg/img/icon-accounts.svg" alt="Persons" width="20" height="20">
|
||||
Persons</a>
|
||||
<a href="/ui/admin/groups" hx-target="#main" class="list-group-item list-group-item-action (% block groups_item_extra_classes%)(%endblock%)">
|
||||
<img src="/pkg/img/icon-groups.svg" alt="Groups" width="20" height="20">
|
||||
Groups (placeholder)</a>
|
||||
<a href="/ui/admin/oauth2" hx-target="#main" class="list-group-item list-group-item-action (% block oauth2_item_extra_classes%)(%endblock%)">
|
||||
<img src="/pkg/img/icon-oauth2.svg" alt="Oauth2" width="20" height="20">
|
||||
Oauth2 (placeholder)</a>
|
||||
</div>
|
||||
<div id="settings-window" class="flex-grow-1 ps-sm-4 pt-sm-0 pt-4">
|
||||
(% block admin_page %)
|
||||
(% endblock %)
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
|
@ -0,0 +1,29 @@
|
|||
(% macro string_attr(dispname, name, value, editable, attribute) %)
|
||||
(% if scim_effective_access.search.check(attribute|as_ref) %)
|
||||
<div class="row mt-3">
|
||||
<label for="person(( name ))" class="col-12 col-md-3 col-lg-2 col-form-label fw-bold py-0">(( dispname ))</label>
|
||||
<div class="col-12 col-md-8 col-lg-6">
|
||||
<input readonly class="form-control-plaintext py-0" id="person(( name ))" name="(( name ))" value="(( value ))">
|
||||
</div>
|
||||
</div>
|
||||
(% endif %)
|
||||
(% endmacro %)
|
||||
|
||||
<form hx-validate="true" hx-ext="bs-validation">
|
||||
(% call string_attr("UUID", "uuid", person.uuid, false, Attribute::Uuid) %)
|
||||
(% call string_attr("SPN", "spn", person.spn, false, Attribute::Spn) %)
|
||||
(% call string_attr("Name", "name", person.name, true, Attribute::Name) %)
|
||||
(% call string_attr("Displayname", "displayname", person.displayname, true, Attribute::DisplayName) %)
|
||||
|
||||
(% if let Some(description) = person.description %)
|
||||
(% call string_attr("Description", "description", description, true, Attribute::Description) %)
|
||||
(% else %)
|
||||
(% call string_attr("Description", "description", "none", true, Attribute::Description) %)
|
||||
(% endif %)
|
||||
|
||||
(% if let Some(entry_managed_by) = person.managed_by %)
|
||||
(% call string_attr("Managed By", "managed_by", entry_managed_by.value, true, Attribute::EntryManagedBy) %)
|
||||
(% else %)
|
||||
(% call string_attr("Managed By", "managed_by", "none", true, Attribute::EntryManagedBy) %)
|
||||
(% endif %)
|
||||
</form>
|
57
server/core/templates/admin/admin_person_view_partial.html
Normal file
57
server/core/templates/admin/admin_person_view_partial.html
Normal file
|
@ -0,0 +1,57 @@
|
|||
(% extends "admin/admin_partial_base.html" %)
|
||||
|
||||
(% block persons_item_extra_classes %)active(% endblock %)
|
||||
|
||||
(% block admin_page %)
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/ui/admin/persons" hx-target="#main">persons Management</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Viewing</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
(% include "admin_person_details_partial.html" %)
|
||||
|
||||
<hr>
|
||||
|
||||
(% if scim_effective_access.search.check(Attribute::Mail|as_ref) %)
|
||||
<label class="mt-3 fw-bold">Emails</label>
|
||||
<form hx-validate="true" hx-ext="bs-validation">
|
||||
(% if person.mails.len() == 0 %)
|
||||
<p>There are no email addresses associated with this person.</p>
|
||||
(% else %)
|
||||
<ol class="list-group col-12 col-md-8 col-lg-6">
|
||||
(% for mail in person.mails %)
|
||||
<li id="personMail(( loop.index ))" class="list-group-item d-flex flex-row justify-content-between">
|
||||
<div class="d-flex align-items-center">(( mail.value ))</div>
|
||||
<div class="buttons float-end">
|
||||
</div>
|
||||
</li>
|
||||
(% endfor %)
|
||||
</ol>
|
||||
(% endif %)
|
||||
</form>
|
||||
(% endif %)
|
||||
|
||||
(% if scim_effective_access.search.check(Attribute::DirectMemberOf|as_ref) %)
|
||||
<label class="mt-3 fw-bold">DirectMemberOf</label>
|
||||
<form hx-validate="true" hx-ext="bs-validation">
|
||||
(% if person.groups.len() == 0 %)
|
||||
<p>There are no groups this person is a direct member of.</p>
|
||||
(% else %)
|
||||
<ol class="list-group col-12 col-md-8 col-lg-6">
|
||||
(% for group in person.groups %)
|
||||
<li id="personGroup(( loop.index ))" class="list-group-item d-flex flex-row justify-content-between">
|
||||
<div class="d-flex align-items-center">(( group.value ))</div>
|
||||
<div class="buttons float-end">
|
||||
</div>
|
||||
</li>
|
||||
(% endfor %)
|
||||
</ol>
|
||||
(% endif %)
|
||||
</form>
|
||||
(% endif %)
|
||||
|
||||
|
||||
|
||||
(% endblock %)
|
23
server/core/templates/admin/admin_persons_partial.html
Normal file
23
server/core/templates/admin/admin_persons_partial.html
Normal file
|
@ -0,0 +1,23 @@
|
|||
(% extends "admin/admin_partial_base.html" %)
|
||||
|
||||
(% block persons_item_extra_classes %)active(% endblock %)
|
||||
|
||||
(% block admin_page %)
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item active" aria-current="page">Person Management</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<ul class="list-group">
|
||||
(% for (person, _) in persons %)
|
||||
<li class="list-group-item d-flex flex-row justify-content-between">
|
||||
<div class="d-flex align-items-center">
|
||||
<a href="/ui/admin/person/(( person.uuid ))/view" hx-target="#main">(( person.name ))</a> <span class="text-secondary d-none d-lg-inline-block mx-4">(( person.uuid ))</span>
|
||||
</div>
|
||||
<div class="buttons float-end">
|
||||
</div>
|
||||
</li>
|
||||
(% endfor %)
|
||||
</ul>
|
||||
(% endblock %)
|
12
server/core/templates/admin/error_toast.html
Normal file
12
server/core/templates/admin/error_toast.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
<div class="toast-container position-fixed bottom-0 end-0 p-3">
|
||||
<div id="permissionDeniedToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="toast-header">
|
||||
<strong class="me-auto">Error</strong>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="toast-body">
|
||||
(( err_code )).<br>
|
||||
OpId: (( operation_id ))
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
11
server/core/templates/admin/saved_toast.html
Normal file
11
server/core/templates/admin/saved_toast.html
Normal file
|
@ -0,0 +1,11 @@
|
|||
<div class="toast-container position-fixed bottom-0 end-0 p-3">
|
||||
<div id="savedToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="toast-header">
|
||||
<strong class="me-auto">Success</strong>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="toast-body">
|
||||
Saved.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
(% block body %)
|
||||
(% include "navbar.html" %)
|
||||
<div id="main">
|
||||
(% block main %)(% endblock %)
|
||||
</div>
|
||||
(% include "signout_modal.html" %)
|
||||
(% endblock %)
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<nav class="navbar navbar-expand-md kanidm_navbar mb-4">
|
||||
<nav hx-boost="false" class="navbar navbar-expand-md kanidm_navbar mb-4">
|
||||
<div class="container-lg">
|
||||
<a class="navbar-brand d-flex align-items-center" href="/ui/apps">
|
||||
(% if navbar_ctx.domain_info.image().is_some() %)
|
||||
|
@ -39,4 +39,4 @@
|
|||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</nav>
|
|
@ -11,6 +11,7 @@ use crate::valueset::{ValueSet, ValueSetIutf8};
|
|||
pub use kanidm_proto::attribute::Attribute;
|
||||
use kanidm_proto::constants::*;
|
||||
use kanidm_proto::internal::OperationError;
|
||||
use kanidm_proto::scim_v1::JsonValue;
|
||||
use kanidm_proto::v1::AccountType;
|
||||
|
||||
use uuid::Uuid;
|
||||
|
@ -129,6 +130,12 @@ impl From<EntryClass> for &'static str {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<EntryClass> for JsonValue {
|
||||
fn from(value: EntryClass) -> Self {
|
||||
Self::String(value.as_ref().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for EntryClass {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.into()
|
||||
|
|
|
@ -23,6 +23,7 @@ use hashbrown::HashMap;
|
|||
use hashbrown::HashSet;
|
||||
use kanidm_proto::constants::ATTR_UUID;
|
||||
use kanidm_proto::internal::{Filter as ProtoFilter, OperationError, SchemaError};
|
||||
use kanidm_proto::scim_v1::client::{AttrPath as ScimAttrPath, ScimFilter};
|
||||
use ldap3_proto::proto::{LdapFilter, LdapSubstringFilter};
|
||||
use serde::Deserialize;
|
||||
use uuid::Uuid;
|
||||
|
@ -764,6 +765,21 @@ impl Filter<FilterInvalid> {
|
|||
},
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(name = "filter::from_scim_ro", level = "trace", skip_all)]
|
||||
pub fn from_scim_ro(
|
||||
ev: &Identity,
|
||||
f: &ScimFilter,
|
||||
qs: &mut QueryServerReadTransaction,
|
||||
) -> Result<Self, OperationError> {
|
||||
let depth = DEFAULT_LIMIT_FILTER_DEPTH_MAX as usize;
|
||||
let mut elems = ev.limits().filter_max_elements;
|
||||
Ok(Filter {
|
||||
state: FilterInvalid {
|
||||
inner: FilterComp::from_scim_ro(f, qs, depth, &mut elems)?,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Filter<FilterInvalid> {
|
||||
|
@ -1087,32 +1103,21 @@ impl FilterComp {
|
|||
elems: &mut usize,
|
||||
) -> Result<Self, OperationError> {
|
||||
let ndepth = depth.checked_sub(1).ok_or(OperationError::ResourceLimit)?;
|
||||
*elems = (*elems)
|
||||
.checked_sub(1)
|
||||
.ok_or(OperationError::ResourceLimit)?;
|
||||
Ok(match f {
|
||||
LdapFilter::And(l) => {
|
||||
*elems = (*elems)
|
||||
.checked_sub(l.len())
|
||||
.ok_or(OperationError::ResourceLimit)?;
|
||||
FilterComp::And(
|
||||
l.iter()
|
||||
.map(|f| Self::from_ldap_ro(f, qs, ndepth, elems))
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
)
|
||||
}
|
||||
LdapFilter::Or(l) => {
|
||||
*elems = (*elems)
|
||||
.checked_sub(l.len())
|
||||
.ok_or(OperationError::ResourceLimit)?;
|
||||
|
||||
FilterComp::Or(
|
||||
l.iter()
|
||||
.map(|f| Self::from_ldap_ro(f, qs, ndepth, elems))
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
)
|
||||
}
|
||||
LdapFilter::And(l) => FilterComp::And(
|
||||
l.iter()
|
||||
.map(|f| Self::from_ldap_ro(f, qs, ndepth, elems))
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
),
|
||||
LdapFilter::Or(l) => FilterComp::Or(
|
||||
l.iter()
|
||||
.map(|f| Self::from_ldap_ro(f, qs, ndepth, elems))
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
),
|
||||
LdapFilter::Not(l) => {
|
||||
*elems = (*elems)
|
||||
.checked_sub(1)
|
||||
.ok_or(OperationError::ResourceLimit)?;
|
||||
FilterComp::AndNot(Box::new(Self::from_ldap_ro(l, qs, ndepth, elems)?))
|
||||
}
|
||||
LdapFilter::Equality(a, v) => {
|
||||
|
@ -1172,6 +1177,103 @@ impl FilterComp {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn from_scim_ro(
|
||||
f: &ScimFilter,
|
||||
qs: &mut QueryServerReadTransaction,
|
||||
depth: usize,
|
||||
elems: &mut usize,
|
||||
) -> Result<Self, OperationError> {
|
||||
let ndepth = depth.checked_sub(1).ok_or(OperationError::ResourceLimit)?;
|
||||
*elems = (*elems)
|
||||
.checked_sub(1)
|
||||
.ok_or(OperationError::ResourceLimit)?;
|
||||
Ok(match f {
|
||||
ScimFilter::Present(ScimAttrPath { a, s: None }) => FilterComp::Pres(a.clone()),
|
||||
ScimFilter::Equal(ScimAttrPath { a, s: None }, json_value) => {
|
||||
let pv = qs.resolve_scim_json_get(a, json_value)?;
|
||||
FilterComp::Eq(a.clone(), pv)
|
||||
}
|
||||
|
||||
ScimFilter::Contains(ScimAttrPath { a, s: None }, json_value) => {
|
||||
let pv = qs.resolve_scim_json_get(a, json_value)?;
|
||||
FilterComp::Cnt(a.clone(), pv)
|
||||
}
|
||||
ScimFilter::StartsWith(ScimAttrPath { a, s: None }, json_value) => {
|
||||
let pv = qs.resolve_scim_json_get(a, json_value)?;
|
||||
FilterComp::Stw(a.clone(), pv)
|
||||
}
|
||||
ScimFilter::EndsWith(ScimAttrPath { a, s: None }, json_value) => {
|
||||
let pv = qs.resolve_scim_json_get(a, json_value)?;
|
||||
FilterComp::Enw(a.clone(), pv)
|
||||
}
|
||||
ScimFilter::Greater(ScimAttrPath { a, s: None }, json_value) => {
|
||||
let pv = qs.resolve_scim_json_get(a, json_value)?;
|
||||
// Greater is equivalent to "not equal or less than".
|
||||
FilterComp::And(vec![
|
||||
FilterComp::Pres(a.clone()),
|
||||
FilterComp::AndNot(Box::new(FilterComp::Or(vec![
|
||||
FilterComp::LessThan(a.clone(), pv.clone()),
|
||||
FilterComp::Eq(a.clone(), pv),
|
||||
]))),
|
||||
])
|
||||
}
|
||||
ScimFilter::Less(ScimAttrPath { a, s: None }, json_value) => {
|
||||
let pv = qs.resolve_scim_json_get(a, json_value)?;
|
||||
FilterComp::LessThan(a.clone(), pv)
|
||||
}
|
||||
ScimFilter::GreaterOrEqual(ScimAttrPath { a, s: None }, json_value) => {
|
||||
let pv = qs.resolve_scim_json_get(a, json_value)?;
|
||||
// Greater or equal is equivalent to "not less than".
|
||||
FilterComp::And(vec![
|
||||
FilterComp::Pres(a.clone()),
|
||||
FilterComp::AndNot(Box::new(FilterComp::LessThan(a.clone(), pv.clone()))),
|
||||
])
|
||||
}
|
||||
ScimFilter::LessOrEqual(ScimAttrPath { a, s: None }, json_value) => {
|
||||
let pv = qs.resolve_scim_json_get(a, json_value)?;
|
||||
FilterComp::Or(vec![
|
||||
FilterComp::LessThan(a.clone(), pv.clone()),
|
||||
FilterComp::Eq(a.clone(), pv),
|
||||
])
|
||||
}
|
||||
ScimFilter::Not(f) => {
|
||||
let f = Self::from_scim_ro(f, qs, ndepth, elems)?;
|
||||
FilterComp::AndNot(Box::new(f))
|
||||
}
|
||||
ScimFilter::Or(left, right) => {
|
||||
let left = Self::from_scim_ro(left, qs, ndepth, elems)?;
|
||||
let right = Self::from_scim_ro(right, qs, ndepth, elems)?;
|
||||
FilterComp::Or(vec![left, right])
|
||||
}
|
||||
ScimFilter::And(left, right) => {
|
||||
let left = Self::from_scim_ro(left, qs, ndepth, elems)?;
|
||||
let right = Self::from_scim_ro(right, qs, ndepth, elems)?;
|
||||
FilterComp::And(vec![left, right])
|
||||
}
|
||||
ScimFilter::NotEqual(ScimAttrPath { s: None, .. }, _) => {
|
||||
error!("Unsupported filter operation - not-equal");
|
||||
return Err(OperationError::FilterGeneration);
|
||||
}
|
||||
ScimFilter::Present(ScimAttrPath { s: Some(_), .. })
|
||||
| ScimFilter::Equal(ScimAttrPath { s: Some(_), .. }, _)
|
||||
| ScimFilter::NotEqual(ScimAttrPath { s: Some(_), .. }, _)
|
||||
| ScimFilter::Contains(ScimAttrPath { s: Some(_), .. }, _)
|
||||
| ScimFilter::StartsWith(ScimAttrPath { s: Some(_), .. }, _)
|
||||
| ScimFilter::EndsWith(ScimAttrPath { s: Some(_), .. }, _)
|
||||
| ScimFilter::Greater(ScimAttrPath { s: Some(_), .. }, _)
|
||||
| ScimFilter::Less(ScimAttrPath { s: Some(_), .. }, _)
|
||||
| ScimFilter::GreaterOrEqual(ScimAttrPath { s: Some(_), .. }, _)
|
||||
| ScimFilter::LessOrEqual(ScimAttrPath { s: Some(_), .. }, _) => {
|
||||
error!("Unsupported filter operation - sub-attribute");
|
||||
return Err(OperationError::FilterGeneration);
|
||||
}
|
||||
ScimFilter::Complex(..) => {
|
||||
error!("Unsupported filter operation - complex");
|
||||
return Err(OperationError::FilterGeneration);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/* We only configure partial eq if cfg test on the invalid/valid types */
|
||||
|
|
|
@ -35,6 +35,7 @@ use concread::arcache::{ARCacheBuilder, ARCacheReadTxn};
|
|||
use concread::cowcell::*;
|
||||
use hashbrown::{HashMap, HashSet};
|
||||
use kanidm_proto::internal::{DomainInfo as ProtoDomainInfo, ImageValue, UiHint};
|
||||
use kanidm_proto::scim_v1::client::ScimFilter;
|
||||
use kanidm_proto::scim_v1::server::ScimOAuth2ClaimMap;
|
||||
use kanidm_proto::scim_v1::server::ScimOAuth2ScopeMap;
|
||||
use kanidm_proto::scim_v1::server::ScimReference;
|
||||
|
@ -934,6 +935,64 @@ pub trait QueryServerTransaction<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
fn resolve_scim_json_get(
|
||||
&mut self,
|
||||
attr: &Attribute,
|
||||
value: &JsonValue,
|
||||
) -> Result<PartialValue, OperationError> {
|
||||
let schema = self.get_schema();
|
||||
// Lookup the attr
|
||||
let Some(schema_a) = schema.get_attributes().get(attr) else {
|
||||
// No attribute of this name exists - fail fast, there is no point to
|
||||
// proceed, as nothing can be satisfied.
|
||||
return Err(OperationError::InvalidAttributeName(attr.to_string()));
|
||||
};
|
||||
|
||||
match schema_a.syntax {
|
||||
SyntaxType::Utf8String => {
|
||||
let JsonValue::String(value) = value else {
|
||||
return Err(OperationError::InvalidAttribute(attr.to_string()));
|
||||
};
|
||||
Ok(PartialValue::Utf8(value.to_string()))
|
||||
}
|
||||
SyntaxType::Utf8StringInsensitive => {
|
||||
let JsonValue::String(value) = value else {
|
||||
return Err(OperationError::InvalidAttribute(attr.to_string()));
|
||||
};
|
||||
Ok(PartialValue::new_iutf8(value))
|
||||
}
|
||||
SyntaxType::Utf8StringIname => {
|
||||
let JsonValue::String(value) = value else {
|
||||
return Err(OperationError::InvalidAttribute(attr.to_string()));
|
||||
};
|
||||
Ok(PartialValue::new_iname(value))
|
||||
}
|
||||
SyntaxType::Uuid => {
|
||||
let JsonValue::String(value) = value else {
|
||||
return Err(OperationError::InvalidAttribute(attr.to_string()));
|
||||
};
|
||||
|
||||
let un = self.name_to_uuid(value).unwrap_or(UUID_DOES_NOT_EXIST);
|
||||
Ok(PartialValue::Uuid(un))
|
||||
}
|
||||
SyntaxType::ReferenceUuid
|
||||
| SyntaxType::OauthScopeMap
|
||||
| SyntaxType::Session
|
||||
| SyntaxType::ApiToken
|
||||
| SyntaxType::Oauth2Session
|
||||
| SyntaxType::ApplicationPassword => {
|
||||
let JsonValue::String(value) = value else {
|
||||
return Err(OperationError::InvalidAttribute(attr.to_string()));
|
||||
};
|
||||
|
||||
let un = self.name_to_uuid(value).unwrap_or(UUID_DOES_NOT_EXIST);
|
||||
Ok(PartialValue::Refer(un))
|
||||
}
|
||||
|
||||
_ => Err(OperationError::InvalidAttribute(attr.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_scim_json_put(
|
||||
&mut self,
|
||||
attr: &Attribute,
|
||||
|
@ -1555,6 +1614,40 @@ impl QueryServerReadTransaction<'_> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip_all)]
|
||||
pub fn scim_search_ext(
|
||||
&mut self,
|
||||
ident: Identity,
|
||||
filter: ScimFilter,
|
||||
query: ScimEntryGetQuery,
|
||||
) -> Result<Vec<ScimEntryKanidm>, OperationError> {
|
||||
let filter_intent = Filter::from_scim_ro(&ident, &filter, self)?;
|
||||
|
||||
let f_intent_valid = filter_intent
|
||||
.validate(self.get_schema())
|
||||
.map_err(OperationError::SchemaViolation)?;
|
||||
|
||||
let f_valid = f_intent_valid.clone().into_ignore_hidden();
|
||||
|
||||
let r_attrs = query
|
||||
.attributes
|
||||
.map(|attr_set| attr_set.into_iter().collect());
|
||||
|
||||
let se = SearchEvent {
|
||||
ident,
|
||||
filter: f_valid,
|
||||
filter_orig: f_intent_valid,
|
||||
attrs: r_attrs,
|
||||
effective_access_check: query.ext_access_check,
|
||||
};
|
||||
|
||||
let vs = self.search_ext(&se)?;
|
||||
|
||||
vs.into_iter()
|
||||
.map(|entry| entry.to_scim_kanidm(self))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> QueryServerTransaction<'a> for QueryServerWriteTransaction<'a> {
|
||||
|
@ -2625,7 +2718,9 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::prelude::*;
|
||||
use kanidm_proto::scim_v1::client::ScimFilter;
|
||||
use kanidm_proto::scim_v1::server::ScimReference;
|
||||
use kanidm_proto::scim_v1::JsonValue;
|
||||
use kanidm_proto::scim_v1::ScimEntryGetQuery;
|
||||
|
||||
#[qs_test]
|
||||
|
@ -3077,4 +3172,44 @@ mod tests {
|
|||
assert!(ext_access_check.modify_present.check(&Attribute::Name));
|
||||
assert!(ext_access_check.modify_remove.check(&Attribute::Name));
|
||||
}
|
||||
|
||||
#[qs_test]
|
||||
async fn test_scim_basic_search_ext_query(server: &QueryServer) {
|
||||
let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
|
||||
|
||||
let group_uuid = Uuid::new_v4();
|
||||
let e1 = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Group.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testgroup")),
|
||||
(Attribute::Uuid, Value::Uuid(group_uuid))
|
||||
);
|
||||
|
||||
assert!(server_txn.internal_create(vec![e1]).is_ok());
|
||||
assert!(server_txn.commit().is_ok());
|
||||
|
||||
// Now read that entry.
|
||||
let mut server_txn = server.read().await.unwrap();
|
||||
|
||||
let idm_admin_entry = server_txn.internal_search_uuid(UUID_IDM_ADMIN).unwrap();
|
||||
let idm_admin_ident = Identity::from_impersonate_entry_readwrite(idm_admin_entry);
|
||||
|
||||
let filter = ScimFilter::And(
|
||||
Box::new(ScimFilter::Equal(
|
||||
Attribute::Class.into(),
|
||||
EntryClass::Group.into(),
|
||||
)),
|
||||
Box::new(ScimFilter::Equal(
|
||||
Attribute::Uuid.into(),
|
||||
JsonValue::String(group_uuid.to_string()),
|
||||
)),
|
||||
);
|
||||
|
||||
let base: Vec<ScimEntryKanidm> = server_txn
|
||||
.scim_search_ext(idm_admin_ident, filter, ScimEntryGetQuery::default())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(base.len(), 1);
|
||||
assert_eq!(base[0].header.id, group_uuid);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue