Compare commits

...

11 commits

Author SHA1 Message Date
William Brown cb287eeb61 Major refactor of migrations to improve durability.
Migrations and server bootstrap are very interconnected processes
and in this we'll be addressing and improving both.

Server bootstrap was performed by creating base entries in phases,
eventually bringing up enough of the *oldest* supported server
minimum remigration level, to then allow triggering of migrations.

Migrations then applied "patches" effectively ontop of this minimum
level to update entries to what they should be in newer versions of
the server.

This scheme has it's pros and cons, but the major con was that to
remove a migration meant squashing it's content back into the
minimum remigration level, and this was a human process that was
quite error prone and difficult to automate. As well, this scheme
also led to cases where the patch migrations would sometimes *not*
reflect all the needed changes or content, or in one case was actually
undone by a patchlevel fix up that was required to address a bug.

Invariably this led to issues, and cases where a new server may have
different content to a migrated one - not exactly what we want!

This is a new migration scheme that addresses this fragility. However
what it trades is verbosity of the content.

Rather than having a base set of entries and patching/updating small
sections ontop, we have migration data folders that contain the full
set of entries as they should appear at that migration level. This
makes the bootstrap process easier as we can just apply the migration
level as a whole, and targetted to what precise version we want.

This also makes migrations more durable as the content is explicitly
copied and all entries fully applied, so there is no risk that a
migration or data change can be forgotten or applied incorrectly. We
are expressing the full state of what our builtin and provided entries
should be.

Finally this rips out a number of places where migration data was being
used as test case data. Not all of these have been replaced (notably
in authsession with Account), but the majority have and have been replaced
with clearer use of constants rather than building whole entries just to
access the name and throw them away for example.
2025-02-22 14:05:04 +10:00
Merlijn 857dcf5087
[htmx] Admin ui for groups and users management ()
* Some progress on admin ui for managing groups and users
* Improve scim querying

---------

Co-authored-by: William Brown <william@blackhats.net.au>
2025-02-22 13:43:54 +10:00
Sebastiano Tocci 9611a7f976
Fixes : add configurable maximum queryable attributes for LDAP () 2025-02-21 12:14:47 +10:00
sinavir f40679cd52
Accept invalid certs and fix token_cache_path ()
* Add accept-invalid-certs option for cli
* Fix token_cache_path behavior

---------

Co-authored-by: sinavir <sinavir@sinavir.fr>
2025-02-20 08:07:48 +00:00
Firstyear 52824b58f1
Accept lowercase ldap pwd hashes () 2025-02-20 04:34:27 +00:00
CEbbinghaus 848af4cecd
TOTP label verification ()
* Adding TOTP Label verification (for both empty and duplicate)
2025-02-19 06:54:50 +00:00
micolous de506a5f53
Rewrite WebFinger docs () 2025-02-19 12:26:15 +10:00
micolous 7f3b1f2580
doc: fix formatting of URL table, remove Caddyfile instructions ()
There are many web servers, and this breaks the flow of the rest of the table.
2025-02-19 11:18:58 +10:00
Alex Martens 9bf17c4846
book: add OAuth2 Proxy example () 2025-02-16 05:14:47 +00:00
Firstyear ed88b72080
Exempt idm_admin and admin from denied names. ()
idm_admin and admin should be exempted from the denied names process,
as these values will already be denied due to attribute uniqueness.
Additionally improved the denied names check to only validate the
name during a change, not during a modifification. This way entries
that become denied can get themself out of the pickle.
2025-02-15 22:45:25 +00:00
Firstyear d0b0b163fd
Book fixes () 2025-02-15 16:01:44 +10:00
88 changed files with 14428 additions and 988 deletions

159
Cargo.lock generated
View file

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

View file

@ -70,36 +70,10 @@ anything special for Kanidm (or another provider).
**Note:** some apps automatically append `/.well-known/openid-configuration` to
the end of an OIDC Discovery URL, so you may need to omit that.
<dl>
<dt>[Webfinger](https://datatracker.ietf.org/doc/html/rfc7033) URL</dt>
<dd>
`https://idm.example.com/oauth2/openid/:client_id:/.well-known/webfinger`
The webfinger URL is implemented for each OpenID client, under its specific endpoint, giving full control to the administrator regarding which to use.
To make this compliant with the standard, it must be made available under the correct [well-known endpoint](https://datatracker.ietf.org/doc/html/rfc7033#section-10.1) (e.g `example.com/.well-known/webfinger`), typically via a reverse proxy or similar. Kanidm doesn't currently provide a mechanism for this URI rewrite.
One example would be dedicating one client as the "primary" or "default" and redirecting all requests to that. Alternatively, source IP or other request metadata could be used to decide which client to forward the request to.
### Caddy
`Caddyfile`
```caddy
# assuming a kanidm service with domain "example.com"
example.com {
redir /.well-known/webfinger https://idm.example.com/oauth2/openid/:client_id:{uri} 307
}
```
**Note:** the `{uri}` is important as it preserves the original request past the redirect.
</dd>
<dt>
[RFC 8414 OAuth 2.0 Authorisation Server Metadata](https://datatracker.ietf.org/doc/html/rfc8414) URL
[RFC 8414 OAuth 2.0 Authorisation Server Metadata](https://datatracker.ietf.org/doc/html/rfc8414)
URL **(recommended)**
</dt>
@ -111,6 +85,21 @@ example.com {
<dt>
[WebFinger URL **(discouraged)**](#webfinger)
</dt>
<dd>
`https://idm.example.com/oauth2/openid/:client_id:/.well-known/webfinger`
See [the WebFinger section](#webfinger) for more details, as there a number of
caveats for WebFinger clients.
</dd>
<dt>
User auth
</dt>
@ -215,7 +204,7 @@ Token signing public key
### Create the Kanidm Configuration
By default, members of the `system_admins` or `idm_hp_oauth2_manage_priv` groups are able to create
By default, members of the `idm_admins` or `idm_oauth2_admins` groups are able to create
or manage OAuth2 client integrations.
You can create a new client by specifying its client name, application display name and the landing
@ -466,3 +455,61 @@ kanidm system oauth2 reset-secrets
```
Each client has unique signing keys and access secrets, so this is limited to each service.
## WebFinger
[WebFinger](https://datatracker.ietf.org/doc/html/rfc7033) provides a mechanism
for discovering information about people or other entities. It can be used by an
identity provider to supply OpenID Connect discovery information.
Kanidm provides
[an Identity Provider Discovery for OIDC URL](https://datatracker.ietf.org/doc/html/rfc7033#section-3.1)
response to all incoming WebFinger requests, using a user's SPN as their account
ID. This does not match on email addresses as they are not guaranteed to be
unique.
However, WebFinger has a number of flaws which make it difficult to use with
Kanidm:
* WebFinger assumes that the identity provider will give the same `iss`
(Issuer) for every OAuth 2.0/OIDC client, and there is no standard way for a
WebFinger client to report its client ID.
Kanidm uses a *different* `iss` (Issuer) value for each client.
* WebFinger requires that this be served at the *root* of the domain of a user's
SPN (ie: information about the user with SPN `user@idm.example.com` is at
`https://idm.example.com/.well-known/webfinger`).
Kanidm *does not* provide a WebFinger endpoint at its root URL, because it has
no way to know *which* OAuth 2.0/OIDC client a WebFinger request is associated
with, so could report an incorrect `iss` (Issuer).
You will need a load balancer in front of Kanidm's HTTPS server to redirect
requests to the appropriate `/oauth2/openid/:client_id:/.well-known/webfinger`
URL. If the client does not follow redirects, you may need to rewrite the
request in the load balancer instead.
If you have *multiple* WebFinger clients, it will need to map some other
property of the request (such as a source IP address or `User-Agent` header)
to a client ID, and redirect to the appropriate WebFinger URL for that client.
* Kanidm responds to *all* WebFinger queries with
[an Identity Provider Discovery for OIDC URL](https://datatracker.ietf.org/doc/html/rfc7033#section-3.1),
**regardless** of what
[`rel` parameter](https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.1)
was specified.
This is to work around
[a broken client](https://tailscale.com/kb/1240/sso-custom-oidc) which doesn't
send a `rel` parameter, but expects an Identity Provider Discovery issuer URL
in response.
If you want to use WebFinger in any *other* context on Kanidm's hostname,
you'll need a load balancer in front of Kanidm which matches on some property
of the request.
Because of the flaws of the WebFinger specification and the deployment
difficulties they introduce, we recommend that applications use OpenID Connect
Discovery or OAuth 2.0 Authorisation Server Metadata for client configuration
instead of WebFinger.

View file

@ -556,6 +556,65 @@ php occ config:app:set --value=0 user_oidc allow_multiple_user_backends
You can login directly by appending `?direct=1` to your login page. You can re-enable other backends
by setting the value to `1`
## OAuth2 Proxy
OAuth2 Proxy is a reverse proxy that provides authentication with OpenID Connect identity providers.
It is typically used to secure web applications without native OpenID Connect support.
Prepare the environment.
Due to a [lack of public client support](https://github.com/oauth2-proxy/oauth2-proxy/issues/1714) we have to set it up as a basic client.
```bash
kanidm system oauth2 create webapp 'webapp.example.com' 'https://webapp.example.com'
kanidm system add-redirect-url webapp 'https://webapp.example.com/oauth2/callback'
kanidm system oauth2 update-scope-map webapp email openid
kanidm system oauth2 get webapp
kanidm system oauth2 show-basic-secret webapp
<SECRET>
```
Create a user group.
```bash
kanidm group create 'webapp_admin'
```
Setup the claim-map to add `webapp_group` to the userinfo claim.
```bash
kanidm system oauth2 update-claim-map-join 'webapp' 'webapp_group' array
kanidm system oauth2 update-claim-map 'webapp' 'webapp_group' 'webapp_admin' 'webapp_admin'
```
Authorize users for the application.
Additionally OAuth2 Proxy requires all users have an email, reference this issue for more details:
- <https://github.com/oauth2-proxy/oauth2-proxy/issues/2667>
```bash
kanidm person update '<user>' --legalname 'Personal Name' --mail 'user@example.com'
kanidm group add-members 'webapp_admin' '<user>'
```
And add the following to your OAuth2 Proxy config.
```toml
provider = "oidc"
scope = "openid email"
# change to match your kanidm domain and client id
oidc_issuer_url = "https://idm.example.com/oauth2/openid/webapp"
# client ID from `kanidm system oauth2 create`
client_id = "webapp"
# redirect URL from `kanidm system add-redirect-url webapp`
redirect_url = "https://webapp.example.com/oauth2/callback"
# claim name from `kanidm system oauth2 update-claim-map-join`
oidc_groups_claim = "webapp_group"
# user group from `kanidm group create`
allowed_groups = ["webapp_admin"]
# secret from `kanidm system oauth2 show-basic-secret webapp`
client_secret = "<SECRET>"
```
## Outline
> These instructions were tested with self-hosted Outline 0.80.2.

View file

@ -22,6 +22,7 @@ This is a list of supported features and standards within Kanidm.
- [RFC4519 LDAP Schema](https://www.rfc-editor.org/rfc/rfc4519)
- FreeIPA User Schema
- [RFC7644 SCIM Bulk Data Import](https://www.rfc-editor.org/rfc/rfc7644)
- NOTE: SCIM is only supported for synchronisation from another IDP at this time.
# Database

View file

@ -30,8 +30,8 @@ use compact_jwt::Jwk;
use kanidm_proto::constants::uri::V1_AUTH_VALID;
use kanidm_proto::constants::{
ATTR_DOMAIN_DISPLAY_NAME, ATTR_DOMAIN_LDAP_BASEDN, ATTR_DOMAIN_SSID, ATTR_ENTRY_MANAGED_BY,
ATTR_KEY_ACTION_REVOKE, ATTR_LDAP_ALLOW_UNIX_PW_BIND, ATTR_NAME, CLIENT_TOKEN_CACHE, KOPID,
KSESSIONID, KVERSION,
ATTR_KEY_ACTION_REVOKE, ATTR_LDAP_ALLOW_UNIX_PW_BIND, ATTR_LDAP_MAX_QUERYABLE_ATTRS, ATTR_NAME,
CLIENT_TOKEN_CACHE, KOPID, KSESSIONID, KVERSION,
};
use kanidm_proto::internal::*;
use kanidm_proto::v1::*;
@ -94,7 +94,7 @@ pub struct KanidmClientConfigInstance {
pub verify_hostnames: Option<bool>,
/// Whether to verify the Certificate Authority details of the server's TLS certificate, defaults to `true`.
///
/// Environment variable is slightly inverted - `KANIDM_SKIP_HOSTNAME_VERIFICATION`.
/// Environment variable is slightly inverted - `KANIDM_ACCEPT_INVALID_CERTS`.
pub verify_ca: Option<bool>,
/// Optionally you can specify the path of a CA certificate to use for verifying the server, if you're not using one trusted by your system certificate store.
///
@ -453,6 +453,13 @@ impl KanidmClientBuilder {
}
}
pub fn set_token_cache_path(self, token_cache_path: Option<String>) -> Self {
KanidmClientBuilder {
token_cache_path,
..self
}
}
#[allow(clippy::result_unit_err)]
pub fn add_root_certificate_filepath(self, ca_path: &str) -> Result<Self, ClientError> {
//Okay we have a ca to add. Let's read it in and setup.
@ -2075,6 +2082,18 @@ impl KanidmClient {
.await
}
/// Sets the maximum number of LDAP attributes that can be queryed in a single operation
pub async fn idm_domain_set_ldap_max_queryable_attrs(
&self,
max_queryable_attrs: usize,
) -> Result<(), ClientError> {
self.perform_put_request(
&format!("/v1/domain/_attr/{}", ATTR_LDAP_MAX_QUERYABLE_ATTRS),
vec![max_queryable_attrs.to_string()],
)
.await
}
pub async fn idm_set_ldap_allow_unix_password_bind(
&self,
enable: bool,

View file

@ -662,9 +662,13 @@ impl TryFrom<&str> for Password {
});
}
// Test 389ds formats
// Test 389ds/openldap formats. Shout outs openldap which sometimes makes these
// lowercase.
if let Some(ds_ssha1) = value.strip_prefix("{SHA}") {
if let Some(ds_ssha1) = value
.strip_prefix("{SHA}")
.or_else(|| value.strip_prefix("{sha}"))
{
let h = general_purpose::STANDARD.decode(ds_ssha1).map_err(|_| ())?;
if h.len() != DS_SHA1_HASH_LEN {
return Err(());
@ -674,7 +678,10 @@ impl TryFrom<&str> for Password {
});
}
if let Some(ds_ssha1) = value.strip_prefix("{SSHA}") {
if let Some(ds_ssha1) = value
.strip_prefix("{SSHA}")
.or_else(|| value.strip_prefix("{ssha}"))
{
let sh = general_purpose::STANDARD.decode(ds_ssha1).map_err(|_| ())?;
let (h, s) = sh.split_at(DS_SHA1_HASH_LEN);
if s.len() != DS_SHA_SALT_LEN {
@ -685,7 +692,10 @@ impl TryFrom<&str> for Password {
});
}
if let Some(ds_ssha256) = value.strip_prefix("{SHA256}") {
if let Some(ds_ssha256) = value
.strip_prefix("{SHA256}")
.or_else(|| value.strip_prefix("{sha256}"))
{
let h = general_purpose::STANDARD
.decode(ds_ssha256)
.map_err(|_| ())?;
@ -697,7 +707,10 @@ impl TryFrom<&str> for Password {
});
}
if let Some(ds_ssha256) = value.strip_prefix("{SSHA256}") {
if let Some(ds_ssha256) = value
.strip_prefix("{SSHA256}")
.or_else(|| value.strip_prefix("{ssha256}"))
{
let sh = general_purpose::STANDARD
.decode(ds_ssha256)
.map_err(|_| ())?;
@ -710,7 +723,10 @@ impl TryFrom<&str> for Password {
});
}
if let Some(ds_ssha512) = value.strip_prefix("{SHA512}") {
if let Some(ds_ssha512) = value
.strip_prefix("{SHA512}")
.or_else(|| value.strip_prefix("{sha512}"))
{
let h = general_purpose::STANDARD
.decode(ds_ssha512)
.map_err(|_| ())?;
@ -722,7 +738,10 @@ impl TryFrom<&str> for Password {
});
}
if let Some(ds_ssha512) = value.strip_prefix("{SSHA512}") {
if let Some(ds_ssha512) = value
.strip_prefix("{SSHA512}")
.or_else(|| value.strip_prefix("{ssha512}"))
{
let sh = general_purpose::STANDARD
.decode(ds_ssha512)
.map_err(|_| ())?;
@ -1441,8 +1460,12 @@ mod tests {
#[test]
fn test_password_from_ds_sha1() {
let im_pw = "{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=";
let _r = Password::try_from(im_pw).expect("Failed to parse");
let im_pw = "{sha}W6ph5Mm5Pz8GgiULbPgzG37mj9g=";
let password = "password";
let r = Password::try_from(im_pw).expect("Failed to parse");
// Known weak, require upgrade.
assert!(r.requires_upgrade());
assert!(r.verify(password).unwrap_or(false));
@ -1451,8 +1474,12 @@ mod tests {
#[test]
fn test_password_from_ds_ssha1() {
let im_pw = "{SSHA}EyzbBiP4u4zxOrLpKTORI/RX3HC6TCTJtnVOCQ==";
let _r = Password::try_from(im_pw).expect("Failed to parse");
let im_pw = "{ssha}EyzbBiP4u4zxOrLpKTORI/RX3HC6TCTJtnVOCQ==";
let password = "password";
let r = Password::try_from(im_pw).expect("Failed to parse");
// Known weak, require upgrade.
assert!(r.requires_upgrade());
assert!(r.verify(password).unwrap_or(false));
@ -1461,8 +1488,12 @@ mod tests {
#[test]
fn test_password_from_ds_sha256() {
let im_pw = "{SHA256}XohImNooBHFR0OVvjcYpJ3NgPQ1qq73WKhHvch0VQtg=";
let _r = Password::try_from(im_pw).expect("Failed to parse");
let im_pw = "{sha256}XohImNooBHFR0OVvjcYpJ3NgPQ1qq73WKhHvch0VQtg=";
let password = "password";
let r = Password::try_from(im_pw).expect("Failed to parse");
// Known weak, require upgrade.
assert!(r.requires_upgrade());
assert!(r.verify(password).unwrap_or(false));
@ -1471,8 +1502,12 @@ mod tests {
#[test]
fn test_password_from_ds_ssha256() {
let im_pw = "{SSHA256}luYWfFJOZgxySTsJXHgIaCYww4yMpu6yest69j/wO5n5OycuHFV/GQ==";
let _r = Password::try_from(im_pw).expect("Failed to parse");
let im_pw = "{ssha256}luYWfFJOZgxySTsJXHgIaCYww4yMpu6yest69j/wO5n5OycuHFV/GQ==";
let password = "password";
let r = Password::try_from(im_pw).expect("Failed to parse");
// Known weak, require upgrade.
assert!(r.requires_upgrade());
assert!(r.verify(password).unwrap_or(false));
@ -1481,8 +1516,12 @@ mod tests {
#[test]
fn test_password_from_ds_sha512() {
let im_pw = "{SHA512}sQnzu7wkTrgkQZF+0G1hi5AI3Qmzvv0bXgc5THBqi7mAsdd4Xll27ASbRt9fEyavWi6m0QP9B8lThf+rDKy8hg==";
let _r = Password::try_from(im_pw).expect("Failed to parse");
let im_pw = "{sha512}sQnzu7wkTrgkQZF+0G1hi5AI3Qmzvv0bXgc5THBqi7mAsdd4Xll27ASbRt9fEyavWi6m0QP9B8lThf+rDKy8hg==";
let password = "password";
let r = Password::try_from(im_pw).expect("Failed to parse");
// Known weak, require upgrade.
assert!(r.requires_upgrade());
assert!(r.verify(password).unwrap_or(false));
@ -1491,8 +1530,12 @@ mod tests {
#[test]
fn test_password_from_ds_ssha512() {
let im_pw = "{SSHA512}JwrSUHkI7FTAfHRVR6KoFlSN0E3dmaQWARjZ+/UsShYlENOqDtFVU77HJLLrY2MuSp0jve52+pwtdVl2QUAHukQ0XUf5LDtM";
let _r = Password::try_from(im_pw).expect("Failed to parse");
let im_pw = "{ssha512}JwrSUHkI7FTAfHRVR6KoFlSN0E3dmaQWARjZ+/UsShYlENOqDtFVU77HJLLrY2MuSp0jve52+pwtdVl2QUAHukQ0XUf5LDtM";
let password = "password";
let r = Password::try_from(im_pw).expect("Failed to parse");
// Known weak, require upgrade.
assert!(r.requires_upgrade());
assert!(r.verify(password).unwrap_or(false));

View file

@ -94,6 +94,7 @@ pub enum Attribute {
LdapEmailAddress,
/// An LDAP Compatible sshkeys virtual attribute
LdapKeys,
LdapMaxQueryableAttrs,
LegalName,
LimitSearchMaxResults,
LimitSearchMaxFilterTest,
@ -322,6 +323,7 @@ impl Attribute {
Attribute::LdapAllowUnixPwBind => ATTR_LDAP_ALLOW_UNIX_PW_BIND,
Attribute::LdapEmailAddress => ATTR_LDAP_EMAIL_ADDRESS,
Attribute::LdapKeys => ATTR_LDAP_KEYS,
Attribute::LdapMaxQueryableAttrs => ATTR_LDAP_MAX_QUERYABLE_ATTRS,
Attribute::LdapSshPublicKey => ATTR_LDAP_SSHPUBLICKEY,
Attribute::LegalName => ATTR_LEGALNAME,
Attribute::LimitSearchMaxResults => ATTR_LIMIT_SEARCH_MAX_RESULTS,
@ -505,6 +507,7 @@ impl Attribute {
ATTR_LDAP_ALLOW_UNIX_PW_BIND => Attribute::LdapAllowUnixPwBind,
ATTR_LDAP_EMAIL_ADDRESS => Attribute::LdapEmailAddress,
ATTR_LDAP_KEYS => Attribute::LdapKeys,
ATTR_LDAP_MAX_QUERYABLE_ATTRS => Attribute::LdapMaxQueryableAttrs,
ATTR_SSH_PUBLICKEY => Attribute::SshPublicKey,
ATTR_LEGALNAME => Attribute::LegalName,
ATTR_LINKEDGROUP => Attribute::LinkedGroup,
@ -628,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;

View file

@ -39,6 +39,8 @@ pub const DEFAULT_SERVER_ADDRESS: &str = "127.0.0.1:8443";
pub const DEFAULT_SERVER_LOCALHOST: &str = "localhost:8443";
/// The default LDAP bind address for the Kanidm client
pub const DEFAULT_LDAP_LOCALHOST: &str = "localhost:636";
/// The default amount of attributes that can be queried in LDAP
pub const DEFAULT_LDAP_MAXIMUM_QUERYABLE_ATTRIBUTES: usize = 16;
/// Default replication configuration
pub const DEFAULT_REPLICATION_ADDRESS: &str = "127.0.0.1:8444";
pub const DEFAULT_REPLICATION_ORIGIN: &str = "repl://localhost:8444";
@ -102,6 +104,7 @@ pub const ATTR_DYNGROUP_FILTER: &str = "dyngroup_filter";
pub const ATTR_DYNGROUP: &str = "dyngroup";
pub const ATTR_DYNMEMBER: &str = "dynmember";
pub const ATTR_LDAP_EMAIL_ADDRESS: &str = "emailaddress";
pub const ATTR_LDAP_MAX_QUERYABLE_ATTRS: &str = "ldap_max_queryable_attrs";
pub const ATTR_EMAIL_ALTERNATIVE: &str = "emailalternative";
pub const ATTR_EMAIL_PRIMARY: &str = "emailprimary";
pub const ATTR_EMAIL: &str = "email";
@ -217,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";

View file

@ -130,6 +130,7 @@ pub enum CURegState {
None,
TotpCheck(TotpSecret),
TotpTryAgain,
TotpNameTryAgain(String),
TotpInvalidSha1,
BackupCodes(Vec<String>),
Passkey(CreationChallengeResponse),

View file

@ -222,6 +222,7 @@ pub enum OperationError {
MG0006SKConstraintsNotMet,
MG0007Oauth2StrictConstraintsNotMet,
MG0008SkipUpgradeAttempted,
MG0009InvalidTargetLevelForBootstrap,
//
KP0001KeyProviderNotLoaded,
KP0002KeyProviderInvalidClass,
@ -462,6 +463,7 @@ impl OperationError {
Self::MG0006SKConstraintsNotMet => Some("Migration Constraints Not Met - Security Keys should not be present.".into()),
Self::MG0007Oauth2StrictConstraintsNotMet => Some("Migration Constraints Not Met - All OAuth2 clients must have strict-redirect-uri mode enabled.".into()),
Self::MG0008SkipUpgradeAttempted => Some("Skip Upgrade Attempted.".into()),
Self::MG0009InvalidTargetLevelForBootstrap => Some("The request target domain level was not valid for bootstrapping a new server instance".into()),
Self::PL0001GidOverlapsSystemRange => None,
Self::SC0001IncomingSshPublicKey => None,
Self::SC0002ReferenceSyntaxInvalid => Some("A SCIM Reference Set contained invalid syntax and can not be processed.".into()),

View file

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

View file

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

View file

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

View file

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

View file

@ -116,7 +116,6 @@ pub struct ServerConfig {
///
/// If unset, the LDAP server will be disabled.
pub ldapbindaddress: Option<String>,
/// The role of this server, one of write_replica, write_replica_no_ui, read_only_replica, defaults to [ServerRole::WriteReplica]
#[serde(default)]
pub role: ServerRole,

View file

@ -1396,6 +1396,7 @@ pub async fn credential_update_update(
return Err(WebError::InternalServerError(errmsg));
}
};
let session_token = match serde_json::from_value(cubody[1].clone()) {
Ok(val) => val,
Err(err) => {
@ -1406,6 +1407,7 @@ pub async fn credential_update_update(
};
debug!("session_token: {:?}", session_token);
debug!("scr: {:?}", scr);
state
.qe_r_ref
.handle_idmcredentialupdate(session_token, scr, kopid.eventid)

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -210,6 +210,8 @@ pub(crate) struct TotpInit {
pub(crate) struct TotpCheck {
wrong_code: bool,
broken_app: bool,
bad_name: bool,
taken_name: Option<String>,
}
#[derive(Template)]
@ -599,6 +601,25 @@ pub(crate) async fn add_totp(
let cu_session_token = get_cu_session(&jar).await?;
let check_totpcode = u32::from_str(&new_totp_form.check_totpcode).unwrap_or_default();
let swapped_handler_trigger =
HxResponseTrigger::after_swap([HxEvent::new("addTotpSwapped".to_string())]);
// If the user has not provided a name or added only spaces we exit early
if new_totp_form.name.trim().is_empty() {
return Ok((
swapped_handler_trigger,
AddTotpPartial {
totp_init: None,
totp_name: "".into(),
totp_value: new_totp_form.check_totpcode.clone(),
check: TotpCheck {
bad_name: true,
..Default::default()
},
},
)
.into_response());
}
let cu_status = if new_totp_form.ignore_broken_app {
// Cope with SHA1 apps because the user has intended to do so, their totp code was already verified
@ -624,6 +645,10 @@ pub(crate) async fn add_totp(
wrong_code: true,
..Default::default()
},
CURegState::TotpNameTryAgain(val) => TotpCheck {
taken_name: Some(val.clone()),
..Default::default()
},
CURegState::TotpInvalidSha1 => TotpCheck {
broken_app: true,
..Default::default()
@ -646,9 +671,6 @@ pub(crate) async fn add_totp(
new_totp_form.check_totpcode.clone()
};
let swapped_handler_trigger =
HxResponseTrigger::after_swap([HxEvent::new("addTotpSwapped".to_string())]);
Ok((
swapped_handler_trigger,
AddTotpPartial {

View file

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

View file

@ -0,0 +1,10 @@
(% extends "base_htmx_with_nav.html" %)
(% block title %)Admin Panel(% endblock %)
(% block head %)
(% endblock %)
(% block main %)
(( partial|safe ))
(% endblock %)

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

View file

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

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

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

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

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

View file

@ -2,7 +2,9 @@
(% block body %)
(% include "navbar.html" %)
<div id="main">
(% block main %)(% endblock %)
</div>
(% include "signout_modal.html" %)
(% endblock %)

View file

@ -19,7 +19,8 @@
<label for="new-totp-name" class="form-label">Enter a name for your TOTP</label>
<input
aria-describedby="totp-name-validation-feedback"
class="form-control"
class="form-control (%- if let Some(_) = check.taken_name -%)is-invalid(%- endif -%)
(%- if check.bad_name -%)is-invalid(%- endif -%)"
name="name"
id="new-totp-name"
value="(( totp_name ))"
@ -51,6 +52,18 @@
<li>Incorrect TOTP code - Please try again</li>
</ul>
</div>
(% else if check.bad_name %)
<div id="neq-totp-validation-feedback">
<ul>
<li>The name you provided was empty or blank. Please provide a proper name</li>
</ul>
</div>
(% else if let Some(name) = check.taken_name %)
<div id="neq-totp-validation-feedback">
<ul>
<li>The name "((name))" is either invalid or already taken, Please pick a different one</li>
</ul>
</div>
(% endif %)
</form>

View file

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

View file

@ -1,19 +1,12 @@
//! Constant Entries for the IDM
use std::fmt::Display;
use crate::constants::groups::idm_builtin_admin_groups;
use crate::constants::uuids::*;
use crate::entry::{Entry, EntryInit, EntryInitNew, EntryNew};
use crate::idm::account::Account;
use crate::value::PartialValue;
use crate::value::Value;
use crate::valueset::{ValueSet, ValueSetIutf8};
pub use kanidm_proto::attribute::Attribute;
use kanidm_proto::constants::*;
use kanidm_proto::internal::OperationError;
use kanidm_proto::v1::AccountType;
use uuid::Uuid;
use kanidm_proto::scim_v1::JsonValue;
//TODO: This would do well in the proto lib
// together with all the other definitions.
@ -129,6 +122,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()
@ -195,158 +194,9 @@ impl EntryClass {
}
}
lazy_static! {
/// Builtin System Admin account.
pub static ref BUILTIN_ACCOUNT_IDM_ADMIN: BuiltinAccount = BuiltinAccount {
account_type: AccountType::ServiceAccount,
entry_managed_by: None,
name: "idm_admin",
uuid: UUID_IDM_ADMIN,
description: "Builtin IDM Admin account.",
displayname: "IDM Administrator",
};
pub static ref E_SYSTEM_INFO_V1: EntryInitNew = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::SystemInfo.to_value()),
(Attribute::Class, EntryClass::System.to_value()),
(Attribute::Uuid, Value::Uuid(UUID_SYSTEM_INFO)),
(
Attribute::Description,
Value::new_utf8s("System (local) info and metadata object.")
),
(Attribute::Version, Value::Uint32(20))
);
pub static ref E_DOMAIN_INFO_DL6: EntryInitNew = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::DomainInfo.to_value()),
(Attribute::Class, EntryClass::System.to_value()),
(Attribute::Class, EntryClass::KeyObject.to_value()),
(Attribute::Class, EntryClass::KeyObjectJwtEs256.to_value()),
(Attribute::Class, EntryClass::KeyObjectJweA128GCM.to_value()),
(Attribute::Name, Value::new_iname("domain_local")),
(Attribute::Uuid, Value::Uuid(UUID_DOMAIN_INFO)),
(
Attribute::Description,
Value::new_utf8s("This local domain's info and metadata object.")
)
);
}
#[derive(Debug, Clone)]
/// Built in accounts such as anonymous, idm_admin and admin
pub struct BuiltinAccount {
pub account_type: kanidm_proto::v1::AccountType,
pub entry_managed_by: Option<uuid::Uuid>,
pub name: &'static str,
pub uuid: Uuid,
pub description: &'static str,
pub displayname: &'static str,
}
impl Default for BuiltinAccount {
fn default() -> Self {
BuiltinAccount {
account_type: AccountType::ServiceAccount,
entry_managed_by: None,
name: "",
uuid: Uuid::new_v4(),
description: "<set description>",
displayname: "<set displayname>",
}
}
}
impl From<BuiltinAccount> for Account {
fn from(value: BuiltinAccount) -> Self {
#[allow(clippy::panic)]
if value.uuid >= DYNAMIC_RANGE_MINIMUM_UUID {
panic!("Builtin ACP has invalid UUID! {:?}", value);
}
Account {
name: value.name.to_string(),
uuid: value.uuid,
displayname: value.displayname.to_string(),
spn: format!("{}@example.com", value.name),
mail_primary: None,
mail: Vec::with_capacity(0),
..Default::default()
}
}
}
impl From<BuiltinAccount> for EntryInitNew {
fn from(value: BuiltinAccount) -> Self {
let mut entry = EntryInitNew::new();
entry.add_ava(Attribute::Name, Value::new_iname(value.name));
#[allow(clippy::panic)]
if value.uuid >= DYNAMIC_RANGE_MINIMUM_UUID {
panic!("Builtin ACP has invalid UUID! {:?}", value);
}
entry.add_ava(Attribute::Uuid, Value::Uuid(value.uuid));
entry.add_ava(Attribute::Description, Value::new_utf8s(value.description));
entry.add_ava(Attribute::DisplayName, Value::new_utf8s(value.displayname));
if let Some(entry_manager) = value.entry_managed_by {
entry.add_ava(Attribute::EntryManagedBy, Value::Refer(entry_manager));
}
entry.set_ava(
Attribute::Class,
vec![
EntryClass::Account.to_value(),
EntryClass::MemberOf.to_value(),
EntryClass::Object.to_value(),
],
);
match value.account_type {
AccountType::Person => entry.add_ava(Attribute::Class, EntryClass::Person.to_value()),
AccountType::ServiceAccount => {
entry.add_ava(Attribute::Class, EntryClass::ServiceAccount.to_value())
}
}
entry
}
}
lazy_static! {
/// Builtin System Admin account.
pub static ref BUILTIN_ACCOUNT_ADMIN: BuiltinAccount = BuiltinAccount {
account_type: AccountType::ServiceAccount,
entry_managed_by: None,
name: "admin",
uuid: UUID_ADMIN,
description: "Builtin System Admin account.",
displayname: "System Administrator",
};
}
lazy_static! {
pub static ref BUILTIN_ACCOUNT_ANONYMOUS_DL6: BuiltinAccount = BuiltinAccount {
account_type: AccountType::ServiceAccount,
entry_managed_by: Some(UUID_IDM_ADMINS),
name: "anonymous",
uuid: UUID_ANONYMOUS,
description: "Anonymous access account.",
displayname: "Anonymous",
};
}
pub fn builtin_accounts() -> Vec<&'static BuiltinAccount> {
vec![
&BUILTIN_ACCOUNT_ADMIN,
&BUILTIN_ACCOUNT_IDM_ADMIN,
&BUILTIN_ACCOUNT_ANONYMOUS_DL6,
]
}
// ============ TEST DATA ============
#[cfg(test)]
pub const UUID_TESTPERSON_1: Uuid = ::uuid::uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
#[cfg(test)]
pub const UUID_TESTPERSON_2: Uuid = ::uuid::uuid!("538faac7-4d29-473b-a59d-23023ac19955");
use crate::entry::{Entry, EntryInit, EntryInitNew, EntryNew};
#[cfg(test)]
lazy_static! {
@ -356,7 +206,10 @@ lazy_static! {
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson1")),
(Attribute::DisplayName, Value::new_utf8s("Test Person 1")),
(Attribute::Uuid, Value::Uuid(UUID_TESTPERSON_1))
(
Attribute::Uuid,
Value::Uuid(super::uuids::UUID_TESTPERSON_1)
)
);
pub static ref E_TESTPERSON_2: EntryInitNew = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
@ -364,28 +217,9 @@ lazy_static! {
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson2")),
(Attribute::DisplayName, Value::new_utf8s("Test Person 2")),
(Attribute::Uuid, Value::Uuid(UUID_TESTPERSON_2))
(
Attribute::Uuid,
Value::Uuid(super::uuids::UUID_TESTPERSON_2)
)
);
}
// ⚠️ DOMAIN LEVEL 1 ENTRIES ⚠️
// Future entries need to be added via migrations.
//
// DO NOT MODIFY THIS DEFINITION
/// Build a list of internal admin entries
pub fn idm_builtin_admin_entries() -> Result<Vec<EntryInitNew>, OperationError> {
let mut res: Vec<EntryInitNew> = vec![
BUILTIN_ACCOUNT_ADMIN.clone().into(),
BUILTIN_ACCOUNT_IDM_ADMIN.clone().into(),
];
for group in idm_builtin_admin_groups() {
let g: EntryInitNew = group.clone().try_into()?;
res.push(g);
}
// We need to push anonymous *after* groups due to entry-managed-by
res.push(BUILTIN_ACCOUNT_ANONYMOUS_DL6.clone().into());
Ok(res)
}

View file

@ -1,20 +1,10 @@
// Re-export as needed
pub mod acp;
pub mod entries;
pub mod groups;
mod key_providers;
pub mod schema;
pub mod system_config;
pub mod uuids;
pub mod values;
pub use self::acp::*;
pub use self::entries::*;
pub use self::groups::*;
pub use self::key_providers::*;
pub use self::schema::*;
pub use self::system_config::*;
pub use self::uuids::*;
pub use self::values::*;

View file

@ -6,6 +6,7 @@ use uuid::{uuid, Uuid};
pub const STR_UUID_ADMIN: &str = "00000000-0000-0000-0000-000000000000";
pub const UUID_ADMIN: Uuid = uuid!("00000000-0000-0000-0000-000000000000");
pub const UUID_IDM_ADMINS: Uuid = uuid!("00000000-0000-0000-0000-000000000001");
pub const NAME_IDM_ADMINS: &str = "idm_admins";
pub const UUID_IDM_PEOPLE_PII_READ: Uuid = uuid!("00000000-0000-0000-0000-000000000002");
pub const UUID_IDM_PEOPLE_WRITE_PRIV: Uuid = uuid!("00000000-0000-0000-0000-000000000003");
pub const UUID_IDM_GROUP_WRITE_PRIV: Uuid = uuid!("00000000-0000-0000-0000-000000000004");
@ -26,6 +27,8 @@ pub const UUID_IDM_ADMIN: Uuid = uuid!("00000000-0000-0000-0000-000000000018");
pub const STR_UUID_SYSTEM_ADMINS: &str = "00000000-0000-0000-0000-000000000019";
pub const UUID_SYSTEM_ADMINS: Uuid = uuid!("00000000-0000-0000-0000-000000000019");
pub const NAME_SYSTEM_ADMINS: &str = "system_admins";
pub const UUID_DOMAIN_ADMINS: Uuid = uuid!("00000000-0000-0000-0000-000000000020");
pub const UUID_IDM_ACCOUNT_UNIX_EXTEND_PRIV: Uuid = uuid!("00000000-0000-0000-0000-000000000021");
pub const UUID_IDM_GROUP_UNIX_EXTEND_PRIV: Uuid = uuid!("00000000-0000-0000-0000-000000000022");
@ -50,6 +53,7 @@ pub const UUID_IDM_HP_SERVICE_ACCOUNT_INTO_PERSON_MIGRATE_PRIV: Uuid =
pub const UUID_IDM_ALL_PERSONS: Uuid = uuid!("00000000-0000-0000-0000-000000000035");
pub const STR_UUID_IDM_ALL_ACCOUNTS: &str = "00000000-0000-0000-0000-000000000036";
pub const UUID_IDM_ALL_ACCOUNTS: Uuid = uuid!("00000000-0000-0000-0000-000000000036");
pub const NAME_IDM_ALL_ACCOUNTS: &str = "idm_all_accounts";
pub const UUID_IDM_HP_SYNC_ACCOUNT_MANAGE_PRIV: Uuid =
uuid!("00000000-0000-0000-0000-000000000037");
@ -131,7 +135,8 @@ pub const UUID_SCHEMA_ATTR_PRIMARY_CREDENTIAL: Uuid = uuid!("00000000-0000-0000-
pub const UUID_SCHEMA_CLASS_PERSON: Uuid = uuid!("00000000-0000-0000-0000-ffff00000044");
pub const UUID_SCHEMA_CLASS_GROUP: Uuid = uuid!("00000000-0000-0000-0000-ffff00000045");
pub const UUID_SCHEMA_CLASS_ACCOUNT: Uuid = uuid!("00000000-0000-0000-0000-ffff00000046");
// GAP - 47
pub const UUID_SCHEMA_ATTR_LDAP_MAXIMUM_QUERYABLE_ATTRIBUTES: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000187");
pub const UUID_SCHEMA_ATTR_ATTRIBUTENAME: Uuid = uuid!("00000000-0000-0000-0000-ffff00000048");
pub const UUID_SCHEMA_ATTR_CLASSNAME: Uuid = uuid!("00000000-0000-0000-0000-ffff00000049");
pub const UUID_SCHEMA_ATTR_LEGALNAME: Uuid = uuid!("00000000-0000-0000-0000-ffff00000050");
@ -451,3 +456,9 @@ pub const UUID_DOES_NOT_EXIST: Uuid = uuid!("00000000-0000-0000-0000-fffffffffff
pub const UUID_ANONYMOUS: Uuid = uuid!("00000000-0000-0000-0000-ffffffffffff");
pub const DYNAMIC_RANGE_MINIMUM_UUID: Uuid = uuid!("00000000-0000-0000-0001-000000000000");
// ======= test data ======
#[cfg(test)]
pub const UUID_TESTPERSON_1: Uuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
#[cfg(test)]
pub const UUID_TESTPERSON_2: Uuid = uuid!("538faac7-4d29-473b-a59d-23023ac19955");

View file

@ -702,6 +702,13 @@ impl Credential {
}
}
pub(crate) fn has_totp_by_name(&self, label: &str) -> bool {
match &self.type_ {
CredentialType::PasswordMfa(_, totp, _, _) => totp.contains_key(label),
_ => false,
}
}
pub(crate) fn new_from_generatedpassword(pw: Password) -> Self {
Credential {
type_: CredentialType::GeneratedPassword(pw),

View file

@ -636,23 +636,6 @@ impl ModifyEvent {
}
}
/// ⚠️ - Bypass the schema state machine and force the filter to be considered valid.
/// This is a TEST ONLY method and will never be exposed in production.
#[cfg(test)]
pub fn new_impersonate_entry_ser(
e: BuiltinAccount,
filter: Filter<FilterInvalid>,
modlist: ModifyList<ModifyInvalid>,
) -> Self {
let ei: EntryInitNew = e.into();
ModifyEvent {
ident: Identity::from_impersonate_entry_readwrite(Arc::new(ei.into_sealed_committed())),
filter: filter.clone().into_valid(),
filter_orig: filter.into_valid(),
modlist: modlist.into_valid(),
}
}
/// ⚠️ - Bypass the schema state machine and force the filter to be considered valid.
/// This is a TEST ONLY method and will never be exposed in production.
#[cfg(test)]

View file

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

View file

@ -1041,18 +1041,10 @@ impl IdmServerProxyReadTransaction<'_> {
#[cfg(test)]
mod tests {
use crate::idm::account::Account;
use crate::idm::accountpolicy::ResolvedAccountPolicy;
use crate::prelude::*;
use kanidm_proto::internal::UiHint;
#[test]
fn test_idm_account_from_anonymous() {
let account: Account = BUILTIN_ACCOUNT_ANONYMOUS_DL6.clone().into();
debug!("{:?}", account);
// I think that's it? we may want to check anonymous mech ...
}
#[idm_test]
async fn test_idm_account_ui_hints(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
let ct = duration_from_epoch_now();

View file

@ -1716,6 +1716,7 @@ mod tests {
};
use crate::idm::delayed::DelayedAction;
use crate::idm::AuthState;
use crate::migration_data::{BUILTIN_ACCOUNT_ANONYMOUS, BUILTIN_ACCOUNT_TEST_PERSON};
use crate::prelude::*;
use crate::server::keys::KeyObjectInternal;
use crate::utils::readable_password_from_random;
@ -1742,7 +1743,7 @@ mod tests {
let webauthn = create_webauthn();
let anon_account: Account = BUILTIN_ACCOUNT_ANONYMOUS_DL6.clone().into();
let anon_account: Account = BUILTIN_ACCOUNT_ANONYMOUS.clone().into();
let asd = AuthSessionData {
account: anon_account,
@ -1819,7 +1820,7 @@ mod tests {
fn start_session_simple_password_mech(privileged: bool) -> UserAuthToken {
let webauthn = create_webauthn();
// create the ent
let mut account: Account = BUILTIN_ACCOUNT_ADMIN.clone().into();
let mut account: Account = BUILTIN_ACCOUNT_TEST_PERSON.clone().into();
// manually load in a cred
let p = CryptoPolicy::minimum();
let cred = Credential::new_password_only(&p, "test_password").unwrap();
@ -1920,7 +1921,7 @@ mod tests {
sketching::test_init();
let webauthn = create_webauthn();
// create the ent
let mut account: Account = BUILTIN_ACCOUNT_ADMIN.clone().into();
let mut account: Account = BUILTIN_ACCOUNT_TEST_PERSON.clone().into();
// manually load in a cred
let p = CryptoPolicy::minimum();
let cred = Credential::new_password_only(&p, "list@no3IBTyqHu$bad").unwrap();
@ -2087,7 +2088,7 @@ mod tests {
sketching::test_init();
let webauthn = create_webauthn();
// create the ent
let mut account: Account = BUILTIN_ACCOUNT_ADMIN.clone().into();
let mut account: Account = BUILTIN_ACCOUNT_TEST_PERSON.clone().into();
// Setup a fake time stamp for consistency.
let ts = Duration::from_secs(12345);
@ -2264,7 +2265,7 @@ mod tests {
sketching::test_init();
let webauthn = create_webauthn();
// create the ent
let mut account: Account = BUILTIN_ACCOUNT_ADMIN.clone().into();
let mut account: Account = BUILTIN_ACCOUNT_TEST_PERSON.clone().into();
// Setup a fake time stamp for consistency.
let ts = Duration::from_secs(12345);
@ -2440,7 +2441,7 @@ mod tests {
let (audit_tx, mut audit_rx) = unbounded();
let ts = duration_from_epoch_now();
// create the ent
let mut account: Account = BUILTIN_ACCOUNT_ADMIN.clone().into();
let mut account: Account = BUILTIN_ACCOUNT_TEST_PERSON.clone().into();
let (webauthn, mut wa, wan_cred) = setup_webauthn_passkey(account.name.as_str());
@ -2594,7 +2595,7 @@ mod tests {
let (audit_tx, mut audit_rx) = unbounded();
let ts = duration_from_epoch_now();
// create the ent
let mut account: Account = BUILTIN_ACCOUNT_ADMIN.clone().into();
let mut account: Account = BUILTIN_ACCOUNT_TEST_PERSON.clone().into();
let (webauthn, mut wa, wan_cred) = setup_webauthn_securitykey(account.name.as_str());
let pw_good = "test_password";
@ -2787,7 +2788,7 @@ mod tests {
let (audit_tx, mut audit_rx) = unbounded();
let ts = duration_from_epoch_now();
// create the ent
let mut account: Account = BUILTIN_ACCOUNT_ADMIN.clone().into();
let mut account: Account = BUILTIN_ACCOUNT_TEST_PERSON.clone().into();
let (webauthn, mut wa, wan_cred) = setup_webauthn_securitykey(account.name.as_str());
@ -3053,7 +3054,7 @@ mod tests {
sketching::test_init();
let webauthn = create_webauthn();
// create the ent
let mut account: Account = BUILTIN_ACCOUNT_ADMIN.clone().into();
let mut account: Account = BUILTIN_ACCOUNT_TEST_PERSON.clone().into();
// Setup a fake time stamp for consistency.
let ts = Duration::from_secs(12345);
@ -3262,7 +3263,7 @@ mod tests {
sketching::test_init();
let webauthn = create_webauthn();
// create the ent
let mut account: Account = BUILTIN_ACCOUNT_ADMIN.clone().into();
let mut account: Account = BUILTIN_ACCOUNT_TEST_PERSON.clone().into();
// Setup a fake time stamp for consistency.
let ts = Duration::from_secs(12345);

View file

@ -86,6 +86,7 @@ enum MfaRegState {
None,
TotpInit(Totp),
TotpTryAgain(Totp),
TotpNameTryAgain(Totp, String),
TotpInvalidSha1(Totp, Totp, String),
Passkey(Box<CreationChallengeResponse>, PasskeyRegistration),
#[allow(dead_code)]
@ -98,6 +99,7 @@ impl fmt::Debug for MfaRegState {
MfaRegState::None => "MfaRegState::None",
MfaRegState::TotpInit(_) => "MfaRegState::TotpInit",
MfaRegState::TotpTryAgain(_) => "MfaRegState::TotpTryAgain",
MfaRegState::TotpNameTryAgain(_, _) => "MfaRegState::TotpNameTryAgain",
MfaRegState::TotpInvalidSha1(_, _, _) => "MfaRegState::TotpInvalidSha1",
MfaRegState::Passkey(_, _) => "MfaRegState::Passkey",
MfaRegState::AttestedPasskey(_, _) => "MfaRegState::AttestedPasskey",
@ -273,6 +275,7 @@ pub enum MfaRegStateStatus {
None,
TotpCheck(TotpSecret),
TotpTryAgain,
TotpNameTryAgain(String),
TotpInvalidSha1,
BackupCodes(HashSet<String>),
Passkey(CreationChallengeResponse),
@ -283,8 +286,9 @@ impl fmt::Debug for MfaRegStateStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let t = match self {
MfaRegStateStatus::None => "MfaRegStateStatus::None",
MfaRegStateStatus::TotpCheck(_) => "MfaRegStateStatus::TotpCheck(_)",
MfaRegStateStatus::TotpCheck(_) => "MfaRegStateStatus::TotpCheck",
MfaRegStateStatus::TotpTryAgain => "MfaRegStateStatus::TotpTryAgain",
MfaRegStateStatus::TotpNameTryAgain(_) => "MfaRegStateStatus::TotpNameTryAgain",
MfaRegStateStatus::TotpInvalidSha1 => "MfaRegStateStatus::TotpInvalidSha1",
MfaRegStateStatus::BackupCodes(_) => "MfaRegStateStatus::BackupCodes",
MfaRegStateStatus::Passkey(_) => "MfaRegStateStatus::Passkey",
@ -389,6 +393,7 @@ impl Into<CUStatus> for CredentialUpdateSessionStatus {
MfaRegStateStatus::None => CURegState::None,
MfaRegStateStatus::TotpCheck(c) => CURegState::TotpCheck(c),
MfaRegStateStatus::TotpTryAgain => CURegState::TotpTryAgain,
MfaRegStateStatus::TotpNameTryAgain(label) => CURegState::TotpNameTryAgain(label),
MfaRegStateStatus::TotpInvalidSha1 => CURegState::TotpInvalidSha1,
MfaRegStateStatus::BackupCodes(s) => {
CURegState::BackupCodes(s.into_iter().collect())
@ -469,6 +474,9 @@ impl From<&CredentialUpdateSession> for CredentialUpdateSessionStatus {
MfaRegState::TotpInit(token) => MfaRegStateStatus::TotpCheck(
token.to_proto(session.account.name.as_str(), session.issuer.as_str()),
),
MfaRegState::TotpNameTryAgain(_, name) => {
MfaRegStateStatus::TotpNameTryAgain(name.clone())
}
MfaRegState::TotpTryAgain(_) => MfaRegStateStatus::TotpTryAgain,
MfaRegState::TotpInvalidSha1(_, _, _) => MfaRegStateStatus::TotpInvalidSha1,
MfaRegState::Passkey(r, _) => MfaRegStateStatus::Passkey(r.as_ref().clone()),
@ -1899,7 +1907,22 @@ impl IdmServerCredUpdateTransaction<'_> {
match &session.mfaregstate {
MfaRegState::TotpInit(totp_token)
| MfaRegState::TotpTryAgain(totp_token)
| MfaRegState::TotpNameTryAgain(totp_token, _)
| MfaRegState::TotpInvalidSha1(totp_token, _, _) => {
if session
.primary
.as_ref()
.map(|cred| cred.has_totp_by_name(label))
.unwrap_or_default()
|| label.trim().is_empty()
|| !Value::validate_str_escapes(label)
{
// The user is trying to add a second TOTP under the same name. Lets save them from themselves
session.mfaregstate =
MfaRegState::TotpNameTryAgain(totp_token.clone(), label.into());
return Ok(session.deref().into());
}
if totp_token.verify(totp_chal, ct) {
// It was valid. Update the credential.
let ncred = session
@ -3368,10 +3391,39 @@ mod tests {
.credential_primary_check_totp(&cust, ct, chal + 1, "totp")
.expect("Failed to update the primary cred totp");
assert!(matches!(
c_status.mfaregstate,
MfaRegStateStatus::TotpTryAgain
));
assert!(
matches!(c_status.mfaregstate, MfaRegStateStatus::TotpTryAgain),
"{:?}",
c_status.mfaregstate
);
// Check that the user actually put something into the label
let c_status = cutxn
.credential_primary_check_totp(&cust, ct, chal, "")
.expect("Failed to update the primary cred totp");
assert!(
matches!(
c_status.mfaregstate,
MfaRegStateStatus::TotpNameTryAgain(ref val) if val == ""
),
"{:?}",
c_status.mfaregstate
);
// Okay, Now they are trying to be smart...
let c_status = cutxn
.credential_primary_check_totp(&cust, ct, chal, " ")
.expect("Failed to update the primary cred totp");
assert!(
matches!(
c_status.mfaregstate,
MfaRegStateStatus::TotpNameTryAgain(ref val) if val == " "
),
"{:?}",
c_status.mfaregstate
);
let c_status = cutxn
.credential_primary_check_totp(&cust, ct, chal, "totp")
@ -3383,6 +3435,40 @@ mod tests {
_ => false,
});
{
let c_status = cutxn
.credential_primary_init_totp(&cust, ct)
.expect("Failed to update the primary cred password");
// Check the status has the token.
let totp_token: Totp = match c_status.mfaregstate {
MfaRegStateStatus::TotpCheck(secret) => Some(secret.try_into().unwrap()),
_ => None,
}
.expect("Unable to retrieve totp token, invalid state.");
trace!(?totp_token);
let chal = totp_token
.do_totp_duration_from_epoch(&ct)
.expect("Failed to perform totp step");
// They tried to add a second totp under the same name
let c_status = cutxn
.credential_primary_check_totp(&cust, ct, chal, "totp")
.expect("Failed to update the primary cred totp");
assert!(
matches!(
c_status.mfaregstate,
MfaRegStateStatus::TotpNameTryAgain(ref val) if val == "totp"
),
"{:?}",
c_status.mfaregstate
);
assert!(cutxn.credential_update_cancel_mfareg(&cust, ct).is_ok())
}
// Should be okay now!
drop(cutxn);

View file

@ -60,6 +60,7 @@ pub struct LdapServer {
basedn: String,
dnre: Regex,
binddnre: Regex,
max_queryable_attrs: usize,
}
#[derive(Debug)]
@ -79,6 +80,12 @@ impl LdapServer {
.qs_read
.internal_search_uuid(UUID_DOMAIN_INFO)?;
// Get the maximum number of queryable attributes from the domain entry
let max_queryable_attrs = domain_entry
.get_ava_single_uint32(Attribute::LdapMaxQueryableAttrs)
.map(|u| u as usize)
.unwrap_or(DEFAULT_LDAP_MAXIMUM_QUERYABLE_ATTRIBUTES);
let basedn = domain_entry
.get_ava_single_iutf8(Attribute::DomainLdapBasedn)
.map(|s| s.to_string())
@ -154,6 +161,7 @@ impl LdapServer {
basedn,
dnre,
binddnre,
max_queryable_attrs,
})
}
@ -239,11 +247,11 @@ impl LdapServer {
let mut all_attrs = false;
let mut all_op_attrs = false;
// TODO #3406: limit the number of attributes here!
let attrs_len = sr.attrs.len();
if sr.attrs.is_empty() {
// If [], then "all" attrs
all_attrs = true;
} else {
} else if attrs_len < self.max_queryable_attrs {
sr.attrs.iter().for_each(|a| {
if a == "*" {
all_attrs = true;
@ -267,6 +275,12 @@ impl LdapServer {
}
}
})
} else {
admin_error!(
"Too many LDAP attributes requested. Maximum allowed is {}, while your search query had {}",
self.max_queryable_attrs, attrs_len
);
return Err(OperationError::ResourceLimit);
}
// We need to retain this to know what the client requested.
@ -1975,7 +1989,7 @@ mod tests {
let me = ModifyEvent::new_internal_invalid(
filter!(f_eq(
Attribute::Name,
PartialValue::new_iname(BUILTIN_GROUP_PEOPLE_PII_READ.name)
PartialValue::new_iname("idm_people_pii_read")
)),
ModifyList::new_list(vec![Modify::Present(
Attribute::Member,
@ -2631,4 +2645,106 @@ mod tests {
&OperationError::InvalidAttributeName("invalid".to_string()),
);
}
#[idm_test]
async fn test_ldap_maximum_queryable_attributes(
idms: &IdmServer,
_idms_delayed: &IdmServerDelayed,
) {
// Set the max queryable attrs to 2
let mut server_txn = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
let set_ldap_maximum_queryable_attrs = ModifyEvent::new_internal_invalid(
filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_DOMAIN_INFO))),
ModifyList::new_purge_and_set(Attribute::LdapMaxQueryableAttrs, Value::Uint32(2)),
);
assert!(server_txn
.qs_write
.modify(&set_ldap_maximum_queryable_attrs)
.and_then(|_| server_txn.commit())
.is_ok());
let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
let usr_uuid = Uuid::new_v4();
let grp_uuid = Uuid::new_v4();
let app_uuid = Uuid::new_v4();
let app_name = "testapp1";
// Setup person, group and application
{
let e1 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(usr_uuid)),
(Attribute::Description, Value::new_utf8s("testperson1")),
(Attribute::DisplayName, Value::new_utf8s("testperson1"))
);
let e2 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("testgroup1")),
(Attribute::Uuid, Value::Uuid(grp_uuid))
);
let e3 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::ServiceAccount.to_value()),
(Attribute::Class, EntryClass::Application.to_value()),
(Attribute::Name, Value::new_iname(app_name)),
(Attribute::Uuid, Value::Uuid(app_uuid)),
(Attribute::LinkedGroup, Value::Refer(grp_uuid))
);
let ct = duration_from_epoch_now();
let mut server_txn = idms.proxy_write(ct).await.unwrap();
assert!(server_txn
.qs_write
.internal_create(vec![e1, e2, e3])
.and_then(|_| server_txn.commit())
.is_ok());
}
// Setup the anonymous login
let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
assert_eq!(
anon_t.effective_session,
LdapSession::UnixBind(UUID_ANONYMOUS)
);
let invalid_search = SearchRequest {
msgid: 1,
base: "dc=example,dc=com".to_string(),
scope: LdapSearchScope::Subtree,
filter: LdapFilter::Present(Attribute::ObjectClass.to_string()),
attrs: vec![
"objectClass".to_string(),
"cn".to_string(),
"givenName".to_string(),
],
};
let valid_search = SearchRequest {
msgid: 1,
base: "dc=example,dc=com".to_string(),
scope: LdapSearchScope::Subtree,
filter: LdapFilter::Present(Attribute::ObjectClass.to_string()),
attrs: vec!["objectClass: person".to_string()],
};
let invalid_res: Result<Vec<LdapMsg>, OperationError> = ldaps
.do_search(idms, &invalid_search, &anon_t, Source::Internal)
.await;
let valid_res: Result<Vec<LdapMsg>, OperationError> = ldaps
.do_search(idms, &valid_search, &anon_t, Source::Internal)
.await;
assert_eq!(invalid_res, Err(OperationError::ResourceLimit));
assert!(valid_res.is_ok());
}
}

View file

@ -50,6 +50,12 @@ pub mod credential;
pub mod entry;
pub mod event;
pub mod filter;
// If this module is ever made public outside of this crate, firstyear will be extremely sad.
// This is *purely migration data*. Don't even think about using it in test cases for anything
// else.
pub(crate) mod migration_data;
pub mod modify;
pub mod time;
pub(crate) mod utils;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,35 @@
//! Constant Entries for the IDM
use crate::constants::uuids::*;
use crate::migration_data::types::BuiltinAccount;
use kanidm_proto::v1::AccountType;
lazy_static! {
/// Builtin System Admin account.
pub static ref BUILTIN_ACCOUNT_IDM_ADMIN: BuiltinAccount = BuiltinAccount {
account_type: AccountType::ServiceAccount,
entry_managed_by: None,
name: "idm_admin",
uuid: UUID_IDM_ADMIN,
description: "Builtin IDM Admin account.",
displayname: "IDM Administrator",
};
/// Builtin System Admin account.
pub static ref BUILTIN_ACCOUNT_ADMIN: BuiltinAccount = BuiltinAccount {
account_type: AccountType::ServiceAccount,
entry_managed_by: None,
name: "admin",
uuid: UUID_ADMIN,
description: "Builtin System Admin account.",
displayname: "System Administrator",
};
pub static ref BUILTIN_ACCOUNT_ANONYMOUS_DL6: BuiltinAccount = BuiltinAccount {
account_type: AccountType::ServiceAccount,
entry_managed_by: Some(UUID_IDM_ADMINS),
name: "anonymous",
uuid: UUID_ANONYMOUS,
description: "Anonymous access account.",
displayname: "Anonymous",
};
}

View file

@ -0,0 +1,408 @@
use crate::entry::EntryInitNew;
use crate::prelude::*;
use crate::value::CredentialType;
use kanidm_proto::internal::{Filter, OperationError, UiHint};
#[derive(Clone, Debug, Default)]
/// Built-in group definitions
pub struct BuiltinGroup {
pub name: &'static str,
pub description: &'static str,
pub uuid: uuid::Uuid,
pub members: Vec<uuid::Uuid>,
pub entry_managed_by: Option<uuid::Uuid>,
pub dyngroup: bool,
pub dyngroup_filter: Option<Filter>,
pub extra_attributes: Vec<(Attribute, Value)>,
}
impl TryFrom<BuiltinGroup> for EntryInitNew {
type Error = OperationError;
fn try_from(val: BuiltinGroup) -> Result<Self, OperationError> {
let mut entry = EntryInitNew::new();
if val.uuid >= DYNAMIC_RANGE_MINIMUM_UUID {
error!("Builtin ACP has invalid UUID! {:?}", val);
return Err(OperationError::InvalidUuid);
}
entry.add_ava(Attribute::Name, Value::new_iname(val.name));
entry.add_ava(Attribute::Description, Value::new_utf8s(val.description));
// classes for groups
entry.set_ava(
Attribute::Class,
vec![EntryClass::Group.into(), EntryClass::Object.into()],
);
if val.dyngroup {
if !val.members.is_empty() {
return Err(OperationError::InvalidSchemaState(format!(
"Builtin dyngroup {} has members specified, this is not allowed",
val.name
)));
}
entry.add_ava(Attribute::Class, EntryClass::DynGroup.to_value());
match val.dyngroup_filter {
Some(filter) => entry.add_ava(Attribute::DynGroupFilter, Value::JsonFilt(filter)),
None => {
error!(
"No filter specified for dyngroup '{}' this is going to break things!",
val.name
);
return Err(OperationError::FilterGeneration);
}
};
}
if let Some(entry_manager) = val.entry_managed_by {
entry.add_ava(Attribute::EntryManagedBy, Value::Refer(entry_manager));
}
entry.add_ava(Attribute::Uuid, Value::Uuid(val.uuid));
entry.set_ava(
Attribute::Member,
val.members
.into_iter()
.map(Value::Refer)
.collect::<Vec<Value>>(),
);
// add any extra attributes
val.extra_attributes
.into_iter()
.for_each(|(attr, val)| entry.add_ava(attr, val));
// all done!
Ok(entry)
}
}
lazy_static! {
// There are our built in "roles". They encapsulate some higher level collections
// of roles. The intent is to allow a pretty generic and correct by default set
// of these use cases.
pub static ref BUILTIN_GROUP_SYSTEM_ADMINS_V1: BuiltinGroup = BuiltinGroup {
name: NAME_SYSTEM_ADMINS,
description: "Builtin System Administrators Group.",
uuid: UUID_SYSTEM_ADMINS,
entry_managed_by: Some(UUID_SYSTEM_ADMINS),
members: vec![UUID_ADMIN],
..Default::default()
};
pub static ref BUILTIN_GROUP_IDM_ADMINS_V1: BuiltinGroup = BuiltinGroup {
name: NAME_IDM_ADMINS,
description: "Builtin IDM Administrators Group.",
uuid: UUID_IDM_ADMINS,
entry_managed_by: Some(UUID_IDM_ADMINS),
members: vec![UUID_IDM_ADMIN],
..Default::default()
};
pub static ref BUILTIN_GROUP_SERVICE_DESK: BuiltinGroup = BuiltinGroup {
name: "idm_service_desk",
description: "Builtin Service Desk Group.",
uuid: UUID_IDM_SERVICE_DESK,
entry_managed_by: Some(UUID_IDM_ADMINS),
members: vec![],
..Default::default()
};
}
lazy_static! {
// These are the "finer" roles. They encapsulate different concepts in the system.
// The next section is the "system style" roles. These adjust the operation of
// kanidm and relate to it's internals and how it functions.
pub static ref BUILTIN_GROUP_RECYCLE_BIN_ADMINS: BuiltinGroup = BuiltinGroup {
name: "idm_recycle_bin_admins",
description: "Builtin Recycle Bin Administrators Group.",
uuid: UUID_IDM_RECYCLE_BIN_ADMINS,
entry_managed_by: Some(UUID_SYSTEM_ADMINS),
members: vec![UUID_SYSTEM_ADMINS],
..Default::default()
};
/// Builtin IDM Group for granting local domain administration rights and trust administration rights
pub static ref BUILTIN_GROUP_DOMAIN_ADMINS: BuiltinGroup = BuiltinGroup {
name: "domain_admins",
description: "Builtin IDM Group for granting local domain administration rights and trust administration rights.",
uuid: UUID_DOMAIN_ADMINS,
entry_managed_by: Some(UUID_SYSTEM_ADMINS),
members: vec![UUID_SYSTEM_ADMINS],
..Default::default()
};
pub static ref BUILTIN_GROUP_SCHEMA_ADMINS: BuiltinGroup = BuiltinGroup {
name: "idm_schema_admins",
description: "Builtin Schema Administration Group.",
uuid: UUID_IDM_SCHEMA_ADMINS,
entry_managed_by: Some(UUID_SYSTEM_ADMINS),
members: vec![UUID_SYSTEM_ADMINS],
..Default::default()
};
pub static ref BUILTIN_GROUP_ACCESS_CONTROL_ADMINS: BuiltinGroup = BuiltinGroup {
name: "idm_access_control_admins",
description: "Builtin Access Control Administration Group.",
entry_managed_by: Some(UUID_SYSTEM_ADMINS),
uuid: UUID_IDM_ACCESS_CONTROL_ADMINS,
members: vec![UUID_SYSTEM_ADMINS],
..Default::default()
};
// These are the IDM roles. They concern application integration, user permissions
// and credential security management.
/// Builtin IDM Group for managing persons and their account details
pub static ref BUILTIN_GROUP_PEOPLE_ADMINS: BuiltinGroup = BuiltinGroup {
name: "idm_people_admins",
description: "Builtin People Administration Group.",
uuid: UUID_IDM_PEOPLE_ADMINS,
entry_managed_by: Some(UUID_IDM_ADMINS),
members: vec![UUID_IDM_ADMINS],
..Default::default()
};
pub static ref BUILTIN_GROUP_PEOPLE_ON_BOARDING: BuiltinGroup = BuiltinGroup {
name: "idm_people_on_boarding",
description: "Builtin People On Boarding Group.",
uuid: UUID_IDM_PEOPLE_ON_BOARDING,
entry_managed_by: Some(UUID_IDM_ADMINS),
members: vec![],
..Default::default()
};
/// Builtin IDM Group for granting elevated people (personal data) read permissions.
pub static ref BUILTIN_GROUP_PEOPLE_PII_READ: BuiltinGroup = BuiltinGroup {
name: "idm_people_pii_read",
description: "Builtin IDM Group for granting elevated people (personal data) read permissions.",
uuid: UUID_IDM_PEOPLE_PII_READ,
entry_managed_by: Some(UUID_IDM_ADMINS),
members: vec![],
..Default::default()
};
/// Builtin IDM Group for granting people the ability to write to their own name attributes.
pub static ref BUILTIN_GROUP_PEOPLE_SELF_NAME_WRITE_DL7: BuiltinGroup = BuiltinGroup {
name: "idm_people_self_name_write",
description: "Builtin IDM Group denoting users that can write to their own name attributes.",
uuid: UUID_IDM_PEOPLE_SELF_NAME_WRITE,
entry_managed_by: Some(UUID_IDM_ADMINS),
members: vec![
UUID_IDM_ALL_PERSONS
],
..Default::default()
};
pub static ref BUILTIN_GROUP_SERVICE_ACCOUNT_ADMINS: BuiltinGroup = BuiltinGroup {
name: "idm_service_account_admins",
description: "Builtin Service Account Administration Group.",
uuid: UUID_IDM_SERVICE_ACCOUNT_ADMINS,
entry_managed_by: Some(UUID_IDM_ADMINS),
members: vec![UUID_IDM_ADMINS],
..Default::default()
};
/// Builtin IDM Group for managing oauth2 resource server integrations to this authentication domain.
pub static ref BUILTIN_GROUP_OAUTH2_ADMINS: BuiltinGroup = BuiltinGroup {
name: "idm_oauth2_admins",
description: "Builtin Oauth2 Integration Administration Group.",
uuid: UUID_IDM_OAUTH2_ADMINS,
entry_managed_by: Some(UUID_IDM_ADMINS),
members: vec![UUID_IDM_ADMINS],
..Default::default()
};
pub static ref BUILTIN_GROUP_RADIUS_SERVICE_ADMINS: BuiltinGroup = BuiltinGroup {
name: "idm_radius_service_admins",
description: "Builtin Radius Administration Group.",
uuid: UUID_IDM_RADIUS_ADMINS,
entry_managed_by: Some(UUID_IDM_ADMINS),
members: vec![UUID_IDM_ADMINS],
..Default::default()
};
/// Builtin IDM Group for RADIUS server access delegation.
pub static ref BUILTIN_IDM_RADIUS_SERVERS_V1: BuiltinGroup = BuiltinGroup {
name: "idm_radius_servers",
description: "Builtin IDM Group for RADIUS server access delegation.",
uuid: UUID_IDM_RADIUS_SERVERS,
entry_managed_by: Some(UUID_IDM_RADIUS_ADMINS),
members: vec![
],
..Default::default()
};
pub static ref BUILTIN_GROUP_MAIL_SERVICE_ADMINS_DL8: BuiltinGroup = BuiltinGroup {
name: "idm_mail_service_admins",
description: "Builtin Mail Server Administration Group.",
uuid: UUID_IDM_MAIL_ADMINS,
entry_managed_by: Some(UUID_IDM_ADMINS),
members: vec![UUID_IDM_ADMINS],
..Default::default()
};
/// Builtin IDM Group for MAIL server Access delegation.
pub static ref BUILTIN_IDM_MAIL_SERVERS_DL8: BuiltinGroup = BuiltinGroup {
name: "idm_mail_servers",
description: "Builtin IDM Group for MAIL server access delegation.",
uuid: UUID_IDM_MAIL_SERVERS,
entry_managed_by: Some(UUID_IDM_MAIL_ADMINS),
members: vec![
],
..Default::default()
};
pub static ref BUILTIN_GROUP_ACCOUNT_POLICY_ADMINS: BuiltinGroup = BuiltinGroup {
name: "idm_account_policy_admins",
description: "Builtin Account Policy Administration Group.",
uuid: UUID_IDM_ACCOUNT_POLICY_ADMINS,
entry_managed_by: Some(UUID_IDM_ADMINS),
members: vec![UUID_IDM_ADMINS],
..Default::default()
};
/// Builtin IDM Group for managing posix/unix attributes on groups and users.
pub static ref BUILTIN_GROUP_UNIX_ADMINS: BuiltinGroup = BuiltinGroup {
name: "idm_unix_admins",
description: "Builtin Unix Administration Group.",
uuid: UUID_IDM_UNIX_ADMINS,
entry_managed_by: Some(UUID_IDM_ADMINS),
members: vec![UUID_IDM_ADMINS],
..Default::default()
};
/// Builtin IDM Group for managing client authentication certificates.
pub static ref BUILTIN_GROUP_CLIENT_CERTIFICATE_ADMINS_DL7: BuiltinGroup = BuiltinGroup {
name: "idm_client_certificate_admins",
description: "Builtin Client Certificate Administration Group.",
uuid: UUID_IDM_CLIENT_CERTIFICATE_ADMINS,
entry_managed_by: Some(UUID_IDM_ADMINS),
members: vec![UUID_IDM_ADMINS],
..Default::default()
};
/// Builtin IDM Group for granting elevated group write and lifecycle permissions.
pub static ref IDM_GROUP_ADMINS_V1: BuiltinGroup = BuiltinGroup {
name: "idm_group_admins",
description: "Builtin IDM Group for granting elevated group write and lifecycle permissions.",
uuid: UUID_IDM_GROUP_ADMINS,
entry_managed_by: Some(UUID_IDM_ADMINS),
members: vec![UUID_IDM_ADMINS],
..Default::default()
};
/// Self-write of mail
pub static ref IDM_PEOPLE_SELF_MAIL_WRITE_DL7: BuiltinGroup = BuiltinGroup {
name: "idm_people_self_mail_write",
description: "Builtin IDM Group for people accounts to update their own mail.",
uuid: UUID_IDM_PEOPLE_SELF_MAIL_WRITE,
members: Vec::with_capacity(0),
..Default::default()
};
}
// at some point vs code just gives up on syntax highlighting inside lazy_static...
lazy_static! {
pub static ref IDM_ALL_PERSONS: BuiltinGroup = BuiltinGroup {
name: "idm_all_persons",
description: "Builtin IDM dynamic group containing all persons.",
uuid: UUID_IDM_ALL_PERSONS,
members: Vec::with_capacity(0),
dyngroup: true,
dyngroup_filter: Some(
Filter::And(vec![
Filter::Eq(Attribute::Class.to_string(), EntryClass::Person.to_string()),
Filter::Eq(Attribute::Class.to_string(), EntryClass::Account.to_string()),
])
),
extra_attributes: vec![
// Enable account policy by default
(Attribute::Class, EntryClass::AccountPolicy.to_value()),
// Enforce this is a system protected object
(Attribute::Class, EntryClass::System.to_value()),
// MFA By Default
(Attribute::CredentialTypeMinimum, CredentialType::Mfa.into()),
],
..Default::default()
};
pub static ref IDM_ALL_ACCOUNTS: BuiltinGroup = BuiltinGroup {
name: NAME_IDM_ALL_ACCOUNTS,
description: "Builtin IDM dynamic group containing all entries that can authenticate.",
uuid: UUID_IDM_ALL_ACCOUNTS,
members: Vec::with_capacity(0),
dyngroup: true,
dyngroup_filter: Some(
Filter::Eq(Attribute::Class.to_string(), EntryClass::Account.to_string()),
),
extra_attributes: vec![
// Enable account policy by default
(Attribute::Class, EntryClass::AccountPolicy.to_value()),
// Enforce this is a system protected object
(Attribute::Class, EntryClass::System.to_value()),
],
..Default::default()
};
pub static ref IDM_UI_ENABLE_EXPERIMENTAL_FEATURES: BuiltinGroup = BuiltinGroup {
name: "idm_ui_enable_experimental_features",
description: "Members of this group will have access to experimental web UI features.",
uuid: UUID_IDM_UI_ENABLE_EXPERIMENTAL_FEATURES,
entry_managed_by: Some(UUID_IDM_ADMINS),
extra_attributes: vec![
(Attribute::GrantUiHint, Value::UiHint(UiHint::ExperimentalFeatures))
],
..Default::default()
};
/// Members of this group will have access to read the mail attribute of all persons and service accounts.
pub static ref IDM_ACCOUNT_MAIL_READ: BuiltinGroup = BuiltinGroup {
name: "idm_account_mail_read",
description: "Members of this group will have access to read the mail attribute of all persons and service accounts.",
entry_managed_by: Some(UUID_IDM_ACCESS_CONTROL_ADMINS),
uuid: UUID_IDM_ACCOUNT_MAIL_READ,
..Default::default()
};
/// This must be the last group to init to include the UUID of the other high priv groups.
pub static ref IDM_HIGH_PRIVILEGE_DL8: BuiltinGroup = BuiltinGroup {
name: "idm_high_privilege",
uuid: UUID_IDM_HIGH_PRIVILEGE,
entry_managed_by: Some(UUID_IDM_ACCESS_CONTROL_ADMINS),
description: "Builtin IDM provided groups with high levels of access that should be audited and limited in modification.",
members: vec![
UUID_SYSTEM_ADMINS,
UUID_IDM_ADMINS,
UUID_DOMAIN_ADMINS,
UUID_IDM_SERVICE_DESK,
UUID_IDM_RECYCLE_BIN_ADMINS,
UUID_IDM_SCHEMA_ADMINS,
UUID_IDM_ACCESS_CONTROL_ADMINS,
UUID_IDM_OAUTH2_ADMINS,
UUID_IDM_RADIUS_ADMINS,
UUID_IDM_ACCOUNT_POLICY_ADMINS,
UUID_IDM_RADIUS_SERVERS,
UUID_IDM_GROUP_ADMINS,
UUID_IDM_UNIX_ADMINS,
UUID_IDM_PEOPLE_PII_READ,
UUID_IDM_PEOPLE_ADMINS,
UUID_IDM_PEOPLE_ON_BOARDING,
UUID_IDM_SERVICE_ACCOUNT_ADMINS,
UUID_IDM_CLIENT_CERTIFICATE_ADMINS,
UUID_IDM_APPLICATION_ADMINS,
UUID_IDM_MAIL_ADMINS,
UUID_IDM_HIGH_PRIVILEGE,
],
..Default::default()
};
pub static ref BUILTIN_GROUP_APPLICATION_ADMINS_DL8: BuiltinGroup = BuiltinGroup {
name: "idm_application_admins",
uuid: UUID_IDM_APPLICATION_ADMINS,
description: "Builtin Application Administration Group.",
entry_managed_by: Some(UUID_IDM_ADMINS),
members: vec![UUID_IDM_ADMINS],
..Default::default()
};
}

View file

@ -0,0 +1,268 @@
mod access;
pub(super) mod accounts;
mod groups;
mod key_providers;
mod schema;
mod system_config;
use self::access::*;
use self::accounts::*;
use self::groups::*;
use self::key_providers::*;
use self::schema::*;
use self::system_config::*;
use crate::prelude::EntryInitNew;
use kanidm_proto::internal::OperationError;
pub fn phase_1_schema_attrs() -> Vec<EntryInitNew> {
vec![
SCHEMA_ATTR_SYNC_CREDENTIAL_PORTAL.clone().into(),
SCHEMA_ATTR_SYNC_YIELD_AUTHORITY.clone().into(),
SCHEMA_ATTR_ACCOUNT_EXPIRE.clone().into(),
SCHEMA_ATTR_ACCOUNT_VALID_FROM.clone().into(),
SCHEMA_ATTR_API_TOKEN_SESSION.clone().into(),
SCHEMA_ATTR_AUTH_SESSION_EXPIRY.clone().into(),
SCHEMA_ATTR_AUTH_PRIVILEGE_EXPIRY.clone().into(),
SCHEMA_ATTR_AUTH_PASSWORD_MINIMUM_LENGTH.clone().into(),
SCHEMA_ATTR_BADLIST_PASSWORD.clone().into(),
SCHEMA_ATTR_CREDENTIAL_UPDATE_INTENT_TOKEN.clone().into(),
SCHEMA_ATTR_ATTESTED_PASSKEYS.clone().into(),
SCHEMA_ATTR_DOMAIN_DISPLAY_NAME.clone().into(),
SCHEMA_ATTR_DOMAIN_LDAP_BASEDN.clone().into(),
SCHEMA_ATTR_DOMAIN_NAME.clone().into(),
SCHEMA_ATTR_LDAP_ALLOW_UNIX_PW_BIND.clone().into(),
SCHEMA_ATTR_DOMAIN_SSID.clone().into(),
SCHEMA_ATTR_DOMAIN_TOKEN_KEY.clone().into(),
SCHEMA_ATTR_DOMAIN_UUID.clone().into(),
SCHEMA_ATTR_DYNGROUP_FILTER.clone().into(),
SCHEMA_ATTR_EC_KEY_PRIVATE.clone().into(),
SCHEMA_ATTR_ES256_PRIVATE_KEY_DER.clone().into(),
SCHEMA_ATTR_FERNET_PRIVATE_KEY_STR.clone().into(),
SCHEMA_ATTR_GIDNUMBER.clone().into(),
SCHEMA_ATTR_GRANT_UI_HINT.clone().into(),
SCHEMA_ATTR_JWS_ES256_PRIVATE_KEY.clone().into(),
SCHEMA_ATTR_LOGINSHELL.clone().into(),
SCHEMA_ATTR_NAME_HISTORY.clone().into(),
SCHEMA_ATTR_NSUNIQUEID.clone().into(),
SCHEMA_ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE
.clone()
.into(),
SCHEMA_ATTR_OAUTH2_CONSENT_SCOPE_MAP.clone().into(),
SCHEMA_ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE.clone().into(),
SCHEMA_ATTR_OAUTH2_PREFER_SHORT_USERNAME.clone().into(),
SCHEMA_ATTR_OAUTH2_RS_BASIC_SECRET.clone().into(),
SCHEMA_ATTR_OAUTH2_RS_IMPLICIT_SCOPES.clone().into(),
SCHEMA_ATTR_OAUTH2_RS_NAME.clone().into(),
SCHEMA_ATTR_OAUTH2_RS_ORIGIN_LANDING.clone().into(),
SCHEMA_ATTR_OAUTH2_RS_SCOPE_MAP.clone().into(),
SCHEMA_ATTR_OAUTH2_RS_SUP_SCOPE_MAP.clone().into(),
SCHEMA_ATTR_OAUTH2_RS_TOKEN_KEY.clone().into(),
SCHEMA_ATTR_OAUTH2_SESSION.clone().into(),
SCHEMA_ATTR_PASSKEYS.clone().into(),
SCHEMA_ATTR_PRIMARY_CREDENTIAL.clone().into(),
SCHEMA_ATTR_PRIVATE_COOKIE_KEY.clone().into(),
SCHEMA_ATTR_RADIUS_SECRET.clone().into(),
SCHEMA_ATTR_RS256_PRIVATE_KEY_DER.clone().into(),
SCHEMA_ATTR_SSH_PUBLICKEY.clone().into(),
SCHEMA_ATTR_SYNC_COOKIE.clone().into(),
SCHEMA_ATTR_SYNC_TOKEN_SESSION.clone().into(),
SCHEMA_ATTR_UNIX_PASSWORD.clone().into(),
SCHEMA_ATTR_USER_AUTH_TOKEN_SESSION.clone().into(),
SCHEMA_ATTR_DENIED_NAME.clone().into(),
SCHEMA_ATTR_CREDENTIAL_TYPE_MINIMUM.clone().into(),
SCHEMA_ATTR_WEBAUTHN_ATTESTATION_CA_LIST.clone().into(),
// DL4
SCHEMA_ATTR_OAUTH2_RS_CLAIM_MAP_DL4.clone().into(),
SCHEMA_ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT_DL4
.clone()
.into(),
// DL5
// DL6
SCHEMA_ATTR_LIMIT_SEARCH_MAX_RESULTS_DL6.clone().into(),
SCHEMA_ATTR_LIMIT_SEARCH_MAX_FILTER_TEST_DL6.clone().into(),
SCHEMA_ATTR_KEY_INTERNAL_DATA_DL6.clone().into(),
SCHEMA_ATTR_KEY_PROVIDER_DL6.clone().into(),
SCHEMA_ATTR_KEY_ACTION_ROTATE_DL6.clone().into(),
SCHEMA_ATTR_KEY_ACTION_REVOKE_DL6.clone().into(),
SCHEMA_ATTR_KEY_ACTION_IMPORT_JWS_ES256_DL6.clone().into(),
// DL7
SCHEMA_ATTR_PATCH_LEVEL_DL7.clone().into(),
SCHEMA_ATTR_DOMAIN_DEVELOPMENT_TAINT_DL7.clone().into(),
SCHEMA_ATTR_REFERS_DL7.clone().into(),
SCHEMA_ATTR_CERTIFICATE_DL7.clone().into(),
SCHEMA_ATTR_OAUTH2_RS_ORIGIN_DL7.clone().into(),
SCHEMA_ATTR_OAUTH2_STRICT_REDIRECT_URI_DL7.clone().into(),
SCHEMA_ATTR_MAIL_DL7.clone().into(),
SCHEMA_ATTR_LEGALNAME_DL7.clone().into(),
SCHEMA_ATTR_DISPLAYNAME_DL7.clone().into(),
// DL8
SCHEMA_ATTR_LINKED_GROUP_DL8.clone().into(),
SCHEMA_ATTR_APPLICATION_PASSWORD_DL8.clone().into(),
SCHEMA_ATTR_ALLOW_PRIMARY_CRED_FALLBACK_DL8.clone().into(),
// DL9
SCHEMA_ATTR_OAUTH2_DEVICE_FLOW_ENABLE_DL9.clone().into(),
SCHEMA_ATTR_DOMAIN_ALLOW_EASTER_EGGS_DL9.clone().into(),
// DL10
SCHEMA_ATTR_DENIED_NAME_DL10.clone().into(),
SCHEMA_ATTR_LDAP_MAXIMUM_QUERYABLE_ATTRIBUTES.clone().into(),
]
}
pub fn phase_2_schema_classes() -> Vec<EntryInitNew> {
vec![
SCHEMA_CLASS_DYNGROUP.clone().into(),
SCHEMA_CLASS_ORGPERSON.clone().into(),
SCHEMA_CLASS_POSIXACCOUNT.clone().into(),
SCHEMA_CLASS_POSIXGROUP.clone().into(),
SCHEMA_CLASS_SYSTEM_CONFIG.clone().into(),
// DL4
SCHEMA_CLASS_OAUTH2_RS_PUBLIC_DL4.clone().into(),
// DL5
SCHEMA_CLASS_ACCOUNT_DL5.clone().into(),
SCHEMA_CLASS_OAUTH2_RS_BASIC_DL5.clone().into(),
// DL6
SCHEMA_CLASS_GROUP_DL6.clone().into(),
SCHEMA_CLASS_KEY_PROVIDER_DL6.clone().into(),
SCHEMA_CLASS_KEY_PROVIDER_INTERNAL_DL6.clone().into(),
SCHEMA_CLASS_KEY_OBJECT_DL6.clone().into(),
SCHEMA_CLASS_KEY_OBJECT_JWT_ES256_DL6.clone().into(),
SCHEMA_CLASS_KEY_OBJECT_JWE_A128GCM_DL6.clone().into(),
SCHEMA_CLASS_KEY_OBJECT_INTERNAL_DL6.clone().into(),
// DL7
SCHEMA_CLASS_SERVICE_ACCOUNT_DL7.clone().into(),
SCHEMA_CLASS_SYNC_ACCOUNT_DL7.clone().into(),
SCHEMA_CLASS_CLIENT_CERTIFICATE_DL7.clone().into(),
// DL8
SCHEMA_CLASS_ACCOUNT_POLICY_DL8.clone().into(),
SCHEMA_CLASS_APPLICATION_DL8.clone().into(),
SCHEMA_CLASS_PERSON_DL8.clone().into(),
// DL9
SCHEMA_CLASS_OAUTH2_RS_DL9.clone().into(),
// DL10
SCHEMA_CLASS_DOMAIN_INFO_DL10.clone().into(),
]
}
pub fn phase_3_key_provider() -> Vec<EntryInitNew> {
vec![E_KEY_PROVIDER_INTERNAL_DL6.clone()]
}
pub fn phase_4_system_entries() -> Vec<EntryInitNew> {
vec![
E_SYSTEM_INFO_V1.clone(),
E_DOMAIN_INFO_DL6.clone(),
E_SYSTEM_CONFIG_V1.clone(),
]
}
pub fn phase_5_builtin_admin_entries() -> Result<Vec<EntryInitNew>, OperationError> {
Ok(vec![
BUILTIN_ACCOUNT_ADMIN.clone().into(),
BUILTIN_ACCOUNT_IDM_ADMIN.clone().into(),
BUILTIN_GROUP_SYSTEM_ADMINS_V1.clone().try_into()?,
BUILTIN_GROUP_IDM_ADMINS_V1.clone().try_into()?,
// We need to push anonymous *after* groups due to entry-managed-by
BUILTIN_ACCOUNT_ANONYMOUS_DL6.clone().into(),
])
}
pub fn phase_6_builtin_non_admin_entries() -> Result<Vec<EntryInitNew>, OperationError> {
Ok(vec![
BUILTIN_GROUP_DOMAIN_ADMINS.clone().try_into()?,
BUILTIN_GROUP_SCHEMA_ADMINS.clone().try_into()?,
BUILTIN_GROUP_ACCESS_CONTROL_ADMINS.clone().try_into()?,
BUILTIN_GROUP_UNIX_ADMINS.clone().try_into()?,
BUILTIN_GROUP_RECYCLE_BIN_ADMINS.clone().try_into()?,
BUILTIN_GROUP_SERVICE_DESK.clone().try_into()?,
BUILTIN_GROUP_OAUTH2_ADMINS.clone().try_into()?,
BUILTIN_GROUP_RADIUS_SERVICE_ADMINS.clone().try_into()?,
BUILTIN_GROUP_ACCOUNT_POLICY_ADMINS.clone().try_into()?,
BUILTIN_GROUP_PEOPLE_ADMINS.clone().try_into()?,
BUILTIN_GROUP_PEOPLE_PII_READ.clone().try_into()?,
BUILTIN_GROUP_PEOPLE_ON_BOARDING.clone().try_into()?,
BUILTIN_GROUP_SERVICE_ACCOUNT_ADMINS.clone().try_into()?,
BUILTIN_GROUP_MAIL_SERVICE_ADMINS_DL8.clone().try_into()?,
IDM_GROUP_ADMINS_V1.clone().try_into()?,
IDM_ALL_PERSONS.clone().try_into()?,
IDM_ALL_ACCOUNTS.clone().try_into()?,
BUILTIN_IDM_RADIUS_SERVERS_V1.clone().try_into()?,
BUILTIN_IDM_MAIL_SERVERS_DL8.clone().try_into()?,
BUILTIN_GROUP_PEOPLE_SELF_NAME_WRITE_DL7
.clone()
.try_into()?,
IDM_PEOPLE_SELF_MAIL_WRITE_DL7.clone().try_into()?,
BUILTIN_GROUP_CLIENT_CERTIFICATE_ADMINS_DL7
.clone()
.try_into()?,
BUILTIN_GROUP_APPLICATION_ADMINS_DL8.clone().try_into()?,
// Write deps on read.clone().try_into()?, so write must be added first.
// All members must exist before we write HP
IDM_HIGH_PRIVILEGE_DL8.clone().try_into()?,
// other things
IDM_UI_ENABLE_EXPERIMENTAL_FEATURES.clone().try_into()?,
IDM_ACCOUNT_MAIL_READ.clone().try_into()?,
])
}
pub fn phase_7_builtin_access_control_profiles() -> Vec<EntryInitNew> {
vec![
// Built in access controls.
IDM_ACP_RECYCLE_BIN_SEARCH_V1.clone().into(),
IDM_ACP_RECYCLE_BIN_REVIVE_V1.clone().into(),
IDM_ACP_SCHEMA_WRITE_ATTRS_V1.clone().into(),
IDM_ACP_SCHEMA_WRITE_CLASSES_V1.clone().into(),
IDM_ACP_ACP_MANAGE_V1.clone().into(),
IDM_ACP_GROUP_ENTRY_MANAGED_BY_MODIFY_V1.clone().into(),
IDM_ACP_GROUP_ENTRY_MANAGER_V1.clone().into(),
IDM_ACP_SYNC_ACCOUNT_MANAGE_V1.clone().into(),
IDM_ACP_RADIUS_SERVERS_V1.clone().into(),
IDM_ACP_RADIUS_SECRET_MANAGE_V1.clone().into(),
IDM_ACP_PEOPLE_SELF_WRITE_MAIL_V1.clone().into(),
IDM_ACP_ACCOUNT_SELF_WRITE_V1.clone().into(),
IDM_ACP_ALL_ACCOUNTS_POSIX_READ_V1.clone().into(),
IDM_ACP_SYSTEM_CONFIG_ACCOUNT_POLICY_MANAGE_V1
.clone()
.into(),
IDM_ACP_GROUP_UNIX_MANAGE_V1.clone().into(),
IDM_ACP_HP_GROUP_UNIX_MANAGE_V1.clone().into(),
IDM_ACP_GROUP_READ_V1.clone().into(),
IDM_ACP_ACCOUNT_UNIX_EXTEND_V1.clone().into(),
IDM_ACP_PEOPLE_PII_READ_V1.clone().into(),
IDM_ACP_PEOPLE_PII_MANAGE_V1.clone().into(),
IDM_ACP_PEOPLE_READ_V1.clone().into(),
IDM_ACP_PEOPLE_MANAGE_V1.clone().into(),
IDM_ACP_PEOPLE_DELETE_V1.clone().into(),
IDM_ACP_PEOPLE_CREDENTIAL_RESET_V1.clone().into(),
IDM_ACP_HP_PEOPLE_CREDENTIAL_RESET_V1.clone().into(),
IDM_ACP_SERVICE_ACCOUNT_CREATE_V1.clone().into(),
IDM_ACP_SERVICE_ACCOUNT_DELETE_V1.clone().into(),
IDM_ACP_SERVICE_ACCOUNT_ENTRY_MANAGER_V1.clone().into(),
IDM_ACP_SERVICE_ACCOUNT_ENTRY_MANAGED_BY_MODIFY_V1
.clone()
.into(),
IDM_ACP_HP_SERVICE_ACCOUNT_ENTRY_MANAGED_BY_MODIFY_V1
.clone()
.into(),
IDM_ACP_SERVICE_ACCOUNT_MANAGE_V1.clone().into(),
// DL4
// DL5
// DL6
IDM_ACP_PEOPLE_CREATE_DL6.clone().into(),
IDM_ACP_ACCOUNT_MAIL_READ_DL6.clone().into(),
// DL7
IDM_ACP_SELF_NAME_WRITE_DL7.clone().into(),
IDM_ACP_HP_CLIENT_CERTIFICATE_MANAGER_DL7.clone().into(),
// DL8
IDM_ACP_SELF_READ_DL8.clone().into(),
IDM_ACP_SELF_WRITE_DL8.clone().into(),
IDM_ACP_APPLICATION_MANAGE_DL8.clone().into(),
IDM_ACP_APPLICATION_ENTRY_MANAGER_DL8.clone().into(),
IDM_ACP_MAIL_SERVERS_DL8.clone().into(),
IDM_ACP_GROUP_ACCOUNT_POLICY_MANAGE_DL8.clone().into(),
// DL9
IDM_ACP_OAUTH2_MANAGE_DL9.clone().into(),
IDM_ACP_GROUP_MANAGE_DL9.clone().into(),
IDM_ACP_DOMAIN_ADMIN_DL9.clone().into(),
]
}

File diff suppressed because it is too large Load diff

View file

@ -8,6 +8,31 @@ use crate::value::Value;
// This is separated because the password badlist section may become very long
lazy_static! {
pub static ref E_SYSTEM_INFO_V1: EntryInitNew = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::SystemInfo.to_value()),
(Attribute::Class, EntryClass::System.to_value()),
(Attribute::Uuid, Value::Uuid(UUID_SYSTEM_INFO)),
(
Attribute::Description,
Value::new_utf8s("System (local) info and metadata object.")
),
(Attribute::Version, Value::Uint32(20))
);
pub static ref E_DOMAIN_INFO_DL6: EntryInitNew = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::DomainInfo.to_value()),
(Attribute::Class, EntryClass::System.to_value()),
(Attribute::Class, EntryClass::KeyObject.to_value()),
(Attribute::Class, EntryClass::KeyObjectJwtEs256.to_value()),
(Attribute::Class, EntryClass::KeyObjectJweA128GCM.to_value()),
(Attribute::Name, Value::new_iname("domain_local")),
(Attribute::Uuid, Value::Uuid(UUID_DOMAIN_INFO)),
(
Attribute::Description,
Value::new_utf8s("This local domain's info and metadata object.")
)
);
pub static ref E_SYSTEM_CONFIG_V1: EntryInitNew = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::SystemConfig.to_value()),

View file

@ -0,0 +1,35 @@
//! Constant Entries for the IDM
use crate::constants::uuids::*;
use crate::migration_data::types::BuiltinAccount;
use kanidm_proto::v1::AccountType;
lazy_static! {
/// Builtin System Admin account.
pub static ref BUILTIN_ACCOUNT_IDM_ADMIN: BuiltinAccount = BuiltinAccount {
account_type: AccountType::ServiceAccount,
entry_managed_by: None,
name: "idm_admin",
uuid: UUID_IDM_ADMIN,
description: "Builtin IDM Admin account.",
displayname: "IDM Administrator",
};
/// Builtin System Admin account.
pub static ref BUILTIN_ACCOUNT_ADMIN: BuiltinAccount = BuiltinAccount {
account_type: AccountType::ServiceAccount,
entry_managed_by: None,
name: "admin",
uuid: UUID_ADMIN,
description: "Builtin System Admin account.",
displayname: "System Administrator",
};
pub static ref BUILTIN_ACCOUNT_ANONYMOUS_DL6: BuiltinAccount = BuiltinAccount {
account_type: AccountType::ServiceAccount,
entry_managed_by: Some(UUID_IDM_ADMINS),
name: "anonymous",
uuid: UUID_ANONYMOUS,
description: "Anonymous access account.",
displayname: "Anonymous",
};
}

View file

@ -106,7 +106,9 @@ lazy_static! {
members: vec![],
..Default::default()
};
}
lazy_static! {
// These are the "finer" roles. They encapsulate different concepts in the system.
// The next section is the "system style" roles. These adjust the operation of
// kanidm and relate to it's internals and how it functions.
@ -404,46 +406,3 @@ lazy_static! {
..Default::default()
};
}
/// Make a list of all the non-admin BuiltinGroup's that are created by default, doing it in a standard-ish way so we can use it around the platform
pub fn idm_builtin_non_admin_groups() -> Vec<&'static BuiltinGroup> {
// Create any system default schema entries.
vec![
&BUILTIN_GROUP_DOMAIN_ADMINS,
&BUILTIN_GROUP_SCHEMA_ADMINS,
&BUILTIN_GROUP_ACCESS_CONTROL_ADMINS,
&BUILTIN_GROUP_UNIX_ADMINS,
&BUILTIN_GROUP_RECYCLE_BIN_ADMINS,
&BUILTIN_GROUP_SERVICE_DESK,
&BUILTIN_GROUP_OAUTH2_ADMINS,
&BUILTIN_GROUP_RADIUS_SERVICE_ADMINS,
&BUILTIN_GROUP_ACCOUNT_POLICY_ADMINS,
&BUILTIN_GROUP_PEOPLE_ADMINS,
&BUILTIN_GROUP_PEOPLE_PII_READ,
&BUILTIN_GROUP_PEOPLE_ON_BOARDING,
&BUILTIN_GROUP_SERVICE_ACCOUNT_ADMINS,
&BUILTIN_GROUP_MAIL_SERVICE_ADMINS_DL8,
&IDM_GROUP_ADMINS_V1,
&IDM_ALL_PERSONS,
&IDM_ALL_ACCOUNTS,
&BUILTIN_IDM_RADIUS_SERVERS_V1,
&BUILTIN_IDM_MAIL_SERVERS_DL8,
&BUILTIN_GROUP_PEOPLE_SELF_NAME_WRITE_DL7,
&IDM_PEOPLE_SELF_MAIL_WRITE_DL7,
&BUILTIN_GROUP_CLIENT_CERTIFICATE_ADMINS_DL7,
&BUILTIN_GROUP_APPLICATION_ADMINS_DL8,
// Write deps on read, so write must be added first.
// All members must exist before we write HP
&IDM_HIGH_PRIVILEGE_DL8,
// other things
&IDM_UI_ENABLE_EXPERIMENTAL_FEATURES,
&IDM_ACCOUNT_MAIL_READ,
]
}
pub fn idm_builtin_admin_groups() -> Vec<&'static BuiltinGroup> {
vec![
&BUILTIN_GROUP_SYSTEM_ADMINS_V1,
&BUILTIN_GROUP_IDM_ADMINS_V1,
]
}

View file

@ -0,0 +1,18 @@
use crate::constants::entries::{Attribute, EntryClass};
use crate::constants::uuids::UUID_KEY_PROVIDER_INTERNAL;
use crate::entry::{Entry, EntryInit, EntryInitNew, EntryNew};
use crate::value::Value;
lazy_static! {
pub static ref E_KEY_PROVIDER_INTERNAL_DL6: EntryInitNew = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::KeyProvider.to_value()),
(Attribute::Class, EntryClass::KeyProviderInternal.to_value()),
(Attribute::Uuid, Value::Uuid(UUID_KEY_PROVIDER_INTERNAL)),
(Attribute::Name, Value::new_iname("key_provider_internal")),
(
Attribute::Description,
Value::new_utf8s("The default database internal cryptographic key provider.")
)
);
}

View file

@ -0,0 +1,273 @@
mod access;
mod accounts;
mod groups;
mod key_providers;
mod schema;
mod system_config;
use self::access::*;
use self::accounts::*;
use self::groups::*;
use self::key_providers::*;
use self::schema::*;
use self::system_config::*;
use crate::prelude::EntryInitNew;
use kanidm_proto::internal::OperationError;
pub fn phase_1_schema_attrs() -> Vec<EntryInitNew> {
vec![
SCHEMA_ATTR_SYNC_CREDENTIAL_PORTAL.clone().into(),
SCHEMA_ATTR_SYNC_YIELD_AUTHORITY.clone().into(),
SCHEMA_ATTR_ACCOUNT_EXPIRE.clone().into(),
SCHEMA_ATTR_ACCOUNT_VALID_FROM.clone().into(),
SCHEMA_ATTR_API_TOKEN_SESSION.clone().into(),
SCHEMA_ATTR_AUTH_SESSION_EXPIRY.clone().into(),
SCHEMA_ATTR_AUTH_PRIVILEGE_EXPIRY.clone().into(),
SCHEMA_ATTR_AUTH_PASSWORD_MINIMUM_LENGTH.clone().into(),
SCHEMA_ATTR_BADLIST_PASSWORD.clone().into(),
SCHEMA_ATTR_CREDENTIAL_UPDATE_INTENT_TOKEN.clone().into(),
SCHEMA_ATTR_ATTESTED_PASSKEYS.clone().into(),
SCHEMA_ATTR_DOMAIN_DISPLAY_NAME.clone().into(),
SCHEMA_ATTR_DOMAIN_LDAP_BASEDN.clone().into(),
SCHEMA_ATTR_DOMAIN_NAME.clone().into(),
SCHEMA_ATTR_LDAP_ALLOW_UNIX_PW_BIND.clone().into(),
SCHEMA_ATTR_DOMAIN_SSID.clone().into(),
SCHEMA_ATTR_DOMAIN_TOKEN_KEY.clone().into(),
SCHEMA_ATTR_DOMAIN_UUID.clone().into(),
SCHEMA_ATTR_DYNGROUP_FILTER.clone().into(),
SCHEMA_ATTR_EC_KEY_PRIVATE.clone().into(),
SCHEMA_ATTR_ES256_PRIVATE_KEY_DER.clone().into(),
SCHEMA_ATTR_FERNET_PRIVATE_KEY_STR.clone().into(),
SCHEMA_ATTR_GIDNUMBER.clone().into(),
SCHEMA_ATTR_GRANT_UI_HINT.clone().into(),
SCHEMA_ATTR_JWS_ES256_PRIVATE_KEY.clone().into(),
SCHEMA_ATTR_LOGINSHELL.clone().into(),
SCHEMA_ATTR_NAME_HISTORY.clone().into(),
SCHEMA_ATTR_NSUNIQUEID.clone().into(),
SCHEMA_ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE
.clone()
.into(),
SCHEMA_ATTR_OAUTH2_CONSENT_SCOPE_MAP.clone().into(),
SCHEMA_ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE.clone().into(),
SCHEMA_ATTR_OAUTH2_PREFER_SHORT_USERNAME.clone().into(),
SCHEMA_ATTR_OAUTH2_RS_BASIC_SECRET.clone().into(),
SCHEMA_ATTR_OAUTH2_RS_IMPLICIT_SCOPES.clone().into(),
SCHEMA_ATTR_OAUTH2_RS_NAME.clone().into(),
SCHEMA_ATTR_OAUTH2_RS_ORIGIN_LANDING.clone().into(),
SCHEMA_ATTR_OAUTH2_RS_SCOPE_MAP.clone().into(),
SCHEMA_ATTR_OAUTH2_RS_SUP_SCOPE_MAP.clone().into(),
SCHEMA_ATTR_OAUTH2_RS_TOKEN_KEY.clone().into(),
SCHEMA_ATTR_OAUTH2_SESSION.clone().into(),
SCHEMA_ATTR_PASSKEYS.clone().into(),
SCHEMA_ATTR_PRIMARY_CREDENTIAL.clone().into(),
SCHEMA_ATTR_PRIVATE_COOKIE_KEY.clone().into(),
SCHEMA_ATTR_RADIUS_SECRET.clone().into(),
SCHEMA_ATTR_RS256_PRIVATE_KEY_DER.clone().into(),
SCHEMA_ATTR_SSH_PUBLICKEY.clone().into(),
SCHEMA_ATTR_SYNC_COOKIE.clone().into(),
SCHEMA_ATTR_SYNC_TOKEN_SESSION.clone().into(),
SCHEMA_ATTR_UNIX_PASSWORD.clone().into(),
SCHEMA_ATTR_USER_AUTH_TOKEN_SESSION.clone().into(),
SCHEMA_ATTR_DENIED_NAME.clone().into(),
SCHEMA_ATTR_CREDENTIAL_TYPE_MINIMUM.clone().into(),
SCHEMA_ATTR_WEBAUTHN_ATTESTATION_CA_LIST.clone().into(),
// DL4
SCHEMA_ATTR_OAUTH2_RS_CLAIM_MAP_DL4.clone().into(),
SCHEMA_ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT_DL4
.clone()
.into(),
// DL5
// DL6
SCHEMA_ATTR_LIMIT_SEARCH_MAX_RESULTS_DL6.clone().into(),
SCHEMA_ATTR_LIMIT_SEARCH_MAX_FILTER_TEST_DL6.clone().into(),
SCHEMA_ATTR_KEY_INTERNAL_DATA_DL6.clone().into(),
SCHEMA_ATTR_KEY_PROVIDER_DL6.clone().into(),
SCHEMA_ATTR_KEY_ACTION_ROTATE_DL6.clone().into(),
SCHEMA_ATTR_KEY_ACTION_REVOKE_DL6.clone().into(),
SCHEMA_ATTR_KEY_ACTION_IMPORT_JWS_ES256_DL6.clone().into(),
// DL7
SCHEMA_ATTR_PATCH_LEVEL_DL7.clone().into(),
SCHEMA_ATTR_DOMAIN_DEVELOPMENT_TAINT_DL7.clone().into(),
SCHEMA_ATTR_REFERS_DL7.clone().into(),
SCHEMA_ATTR_CERTIFICATE_DL7.clone().into(),
SCHEMA_ATTR_OAUTH2_RS_ORIGIN_DL7.clone().into(),
SCHEMA_ATTR_OAUTH2_STRICT_REDIRECT_URI_DL7.clone().into(),
SCHEMA_ATTR_MAIL_DL7.clone().into(),
SCHEMA_ATTR_LEGALNAME_DL7.clone().into(),
SCHEMA_ATTR_DISPLAYNAME_DL7.clone().into(),
// DL8
SCHEMA_ATTR_LINKED_GROUP_DL8.clone().into(),
SCHEMA_ATTR_APPLICATION_PASSWORD_DL8.clone().into(),
SCHEMA_ATTR_ALLOW_PRIMARY_CRED_FALLBACK_DL8.clone().into(),
]
}
pub fn phase_2_schema_classes() -> Vec<EntryInitNew> {
vec![
SCHEMA_CLASS_DYNGROUP.clone().into(),
SCHEMA_CLASS_ORGPERSON.clone().into(),
SCHEMA_CLASS_POSIXACCOUNT.clone().into(),
SCHEMA_CLASS_POSIXGROUP.clone().into(),
SCHEMA_CLASS_SYSTEM_CONFIG.clone().into(),
// DL4
SCHEMA_CLASS_OAUTH2_RS_PUBLIC_DL4.clone().into(),
// DL5
// SCHEMA_CLASS_PERSON_DL5.clone().into(),
SCHEMA_CLASS_ACCOUNT_DL5.clone().into(),
// SCHEMA_CLASS_OAUTH2_RS_DL5.clone().into(),
SCHEMA_CLASS_OAUTH2_RS_BASIC_DL5.clone().into(),
// DL6
// SCHEMA_CLASS_ACCOUNT_POLICY_DL6.clone().into(),
// SCHEMA_CLASS_SERVICE_ACCOUNT_DL6.clone().into(),
// SCHEMA_CLASS_SYNC_ACCOUNT_DL6.clone().into(),
SCHEMA_CLASS_GROUP_DL6.clone().into(),
SCHEMA_CLASS_KEY_PROVIDER_DL6.clone().into(),
SCHEMA_CLASS_KEY_PROVIDER_INTERNAL_DL6.clone().into(),
SCHEMA_CLASS_KEY_OBJECT_DL6.clone().into(),
SCHEMA_CLASS_KEY_OBJECT_JWT_ES256_DL6.clone().into(),
SCHEMA_CLASS_KEY_OBJECT_JWE_A128GCM_DL6.clone().into(),
SCHEMA_CLASS_KEY_OBJECT_INTERNAL_DL6.clone().into(),
// SCHEMA_CLASS_DOMAIN_INFO_DL6.clone().into(),
// DL7
// SCHEMA_CLASS_DOMAIN_INFO_DL7.clone().into(),
SCHEMA_CLASS_SERVICE_ACCOUNT_DL7.clone().into(),
SCHEMA_CLASS_SYNC_ACCOUNT_DL7.clone().into(),
SCHEMA_CLASS_CLIENT_CERTIFICATE_DL7.clone().into(),
SCHEMA_CLASS_OAUTH2_RS_DL7.clone().into(),
// DL8
SCHEMA_CLASS_ACCOUNT_POLICY_DL8.clone().into(),
SCHEMA_CLASS_APPLICATION_DL8.clone().into(),
SCHEMA_CLASS_PERSON_DL8.clone().into(),
SCHEMA_CLASS_DOMAIN_INFO_DL8.clone().into(),
]
}
pub fn phase_3_key_provider() -> Vec<EntryInitNew> {
vec![E_KEY_PROVIDER_INTERNAL_DL6.clone()]
}
pub fn phase_4_system_entries() -> Vec<EntryInitNew> {
vec![
E_SYSTEM_INFO_V1.clone(),
E_DOMAIN_INFO_DL6.clone(),
E_SYSTEM_CONFIG_V1.clone(),
]
}
pub fn phase_5_builtin_admin_entries() -> Result<Vec<EntryInitNew>, OperationError> {
Ok(vec![
BUILTIN_ACCOUNT_ADMIN.clone().into(),
BUILTIN_ACCOUNT_IDM_ADMIN.clone().into(),
BUILTIN_GROUP_SYSTEM_ADMINS_V1.clone().try_into()?,
BUILTIN_GROUP_IDM_ADMINS_V1.clone().try_into()?,
// We need to push anonymous *after* groups due to entry-managed-by
BUILTIN_ACCOUNT_ANONYMOUS_DL6.clone().into(),
])
}
pub fn phase_6_builtin_non_admin_entries() -> Result<Vec<EntryInitNew>, OperationError> {
Ok(vec![
BUILTIN_GROUP_DOMAIN_ADMINS.clone().try_into()?,
BUILTIN_GROUP_SCHEMA_ADMINS.clone().try_into()?,
BUILTIN_GROUP_ACCESS_CONTROL_ADMINS.clone().try_into()?,
BUILTIN_GROUP_UNIX_ADMINS.clone().try_into()?,
BUILTIN_GROUP_RECYCLE_BIN_ADMINS.clone().try_into()?,
BUILTIN_GROUP_SERVICE_DESK.clone().try_into()?,
BUILTIN_GROUP_OAUTH2_ADMINS.clone().try_into()?,
BUILTIN_GROUP_RADIUS_SERVICE_ADMINS.clone().try_into()?,
BUILTIN_GROUP_ACCOUNT_POLICY_ADMINS.clone().try_into()?,
BUILTIN_GROUP_PEOPLE_ADMINS.clone().try_into()?,
BUILTIN_GROUP_PEOPLE_PII_READ.clone().try_into()?,
BUILTIN_GROUP_PEOPLE_ON_BOARDING.clone().try_into()?,
BUILTIN_GROUP_SERVICE_ACCOUNT_ADMINS.clone().try_into()?,
BUILTIN_GROUP_MAIL_SERVICE_ADMINS_DL8.clone().try_into()?,
IDM_GROUP_ADMINS_V1.clone().try_into()?,
IDM_ALL_PERSONS.clone().try_into()?,
IDM_ALL_ACCOUNTS.clone().try_into()?,
BUILTIN_IDM_RADIUS_SERVERS_V1.clone().try_into()?,
BUILTIN_IDM_MAIL_SERVERS_DL8.clone().try_into()?,
BUILTIN_GROUP_PEOPLE_SELF_NAME_WRITE_DL7
.clone()
.try_into()?,
IDM_PEOPLE_SELF_MAIL_WRITE_DL7.clone().try_into()?,
BUILTIN_GROUP_CLIENT_CERTIFICATE_ADMINS_DL7
.clone()
.try_into()?,
BUILTIN_GROUP_APPLICATION_ADMINS_DL8.clone().try_into()?,
// Write deps on read.clone().try_into()?, so write must be added first.
// All members must exist before we write HP
IDM_HIGH_PRIVILEGE_DL8.clone().try_into()?,
// other things
IDM_UI_ENABLE_EXPERIMENTAL_FEATURES.clone().try_into()?,
IDM_ACCOUNT_MAIL_READ.clone().try_into()?,
])
}
pub fn phase_7_builtin_access_control_profiles() -> Vec<EntryInitNew> {
vec![
// Built in access controls.
IDM_ACP_RECYCLE_BIN_SEARCH_V1.clone().into(),
IDM_ACP_RECYCLE_BIN_REVIVE_V1.clone().into(),
IDM_ACP_SCHEMA_WRITE_ATTRS_V1.clone().into(),
IDM_ACP_SCHEMA_WRITE_CLASSES_V1.clone().into(),
IDM_ACP_ACP_MANAGE_V1.clone().into(),
IDM_ACP_GROUP_ENTRY_MANAGED_BY_MODIFY_V1.clone().into(),
IDM_ACP_GROUP_ENTRY_MANAGER_V1.clone().into(),
IDM_ACP_SYNC_ACCOUNT_MANAGE_V1.clone().into(),
IDM_ACP_RADIUS_SERVERS_V1.clone().into(),
IDM_ACP_RADIUS_SECRET_MANAGE_V1.clone().into(),
IDM_ACP_PEOPLE_SELF_WRITE_MAIL_V1.clone().into(),
// IDM_ACP_SELF_READ_V1.clone(),
// IDM_ACP_SELF_WRITE_V1.clone(),
IDM_ACP_ACCOUNT_SELF_WRITE_V1.clone().into(),
// IDM_ACP_SELF_NAME_WRITE_V1.clone(),
IDM_ACP_ALL_ACCOUNTS_POSIX_READ_V1.clone().into(),
IDM_ACP_SYSTEM_CONFIG_ACCOUNT_POLICY_MANAGE_V1
.clone()
.into(),
IDM_ACP_GROUP_UNIX_MANAGE_V1.clone().into(),
IDM_ACP_HP_GROUP_UNIX_MANAGE_V1.clone().into(),
IDM_ACP_GROUP_READ_V1.clone().into(),
IDM_ACP_ACCOUNT_UNIX_EXTEND_V1.clone().into(),
IDM_ACP_PEOPLE_PII_READ_V1.clone().into(),
IDM_ACP_PEOPLE_PII_MANAGE_V1.clone().into(),
IDM_ACP_PEOPLE_READ_V1.clone().into(),
IDM_ACP_PEOPLE_MANAGE_V1.clone().into(),
IDM_ACP_PEOPLE_DELETE_V1.clone().into(),
IDM_ACP_PEOPLE_CREDENTIAL_RESET_V1.clone().into(),
IDM_ACP_HP_PEOPLE_CREDENTIAL_RESET_V1.clone().into(),
IDM_ACP_SERVICE_ACCOUNT_CREATE_V1.clone().into(),
IDM_ACP_SERVICE_ACCOUNT_DELETE_V1.clone().into(),
IDM_ACP_SERVICE_ACCOUNT_ENTRY_MANAGER_V1.clone().into(),
IDM_ACP_SERVICE_ACCOUNT_ENTRY_MANAGED_BY_MODIFY_V1
.clone()
.into(),
IDM_ACP_HP_SERVICE_ACCOUNT_ENTRY_MANAGED_BY_MODIFY_V1
.clone()
.into(),
IDM_ACP_SERVICE_ACCOUNT_MANAGE_V1.clone().into(),
// DL4
// DL5
// IDM_ACP_OAUTH2_MANAGE_DL5.clone(),
// DL6
// IDM_ACP_GROUP_ACCOUNT_POLICY_MANAGE_DL6.clone(),
IDM_ACP_PEOPLE_CREATE_DL6.clone().into(),
IDM_ACP_GROUP_MANAGE_DL6.clone().into(),
IDM_ACP_ACCOUNT_MAIL_READ_DL6.clone().into(),
// IDM_ACP_DOMAIN_ADMIN_DL6.clone(),
// DL7
// IDM_ACP_SELF_WRITE_DL7.clone(),
IDM_ACP_SELF_NAME_WRITE_DL7.clone().into(),
IDM_ACP_HP_CLIENT_CERTIFICATE_MANAGER_DL7.clone().into(),
IDM_ACP_OAUTH2_MANAGE_DL7.clone().into(),
// DL8
IDM_ACP_SELF_READ_DL8.clone().into(),
IDM_ACP_SELF_WRITE_DL8.clone().into(),
IDM_ACP_APPLICATION_MANAGE_DL8.clone().into(),
IDM_ACP_APPLICATION_ENTRY_MANAGER_DL8.clone().into(),
IDM_ACP_MAIL_SERVERS_DL8.clone().into(),
IDM_ACP_DOMAIN_ADMIN_DL8.clone().into(),
IDM_ACP_GROUP_ACCOUNT_POLICY_MANAGE_DL8.clone().into(),
]
}

View file

@ -1,7 +1,4 @@
//! Core Constants
//!
//! Schema uuids start at `00000000-0000-0000-0000-ffff00000000`
//!
//! Schema Entries
use crate::constants::entries::{Attribute, EntryClass};
use crate::constants::uuids::*;
use crate::schema::{SchemaAttribute, SchemaClass};
@ -208,6 +205,16 @@ pub static ref SCHEMA_ATTR_DENIED_NAME: SchemaAttribute = SchemaAttribute {
..Default::default()
};
pub static ref SCHEMA_ATTR_DENIED_NAME_DL10: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_DENIED_NAME,
name: Attribute::DeniedName,
description: "Iname values that are not allowed to be used in 'name'.".to_string(),
syntax: SyntaxType::Utf8StringIname,
multivalue: true,
..Default::default()
};
pub static ref SCHEMA_ATTR_DOMAIN_TOKEN_KEY: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_DOMAIN_TOKEN_KEY,
name: Attribute::DomainTokenKey,

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,35 @@
//! Constant Entries for the IDM
use crate::constants::uuids::*;
use crate::migration_data::types::BuiltinAccount;
use kanidm_proto::v1::AccountType;
lazy_static! {
/// Builtin System Admin account.
pub static ref BUILTIN_ACCOUNT_IDM_ADMIN: BuiltinAccount = BuiltinAccount {
account_type: AccountType::ServiceAccount,
entry_managed_by: None,
name: "idm_admin",
uuid: UUID_IDM_ADMIN,
description: "Builtin IDM Admin account.",
displayname: "IDM Administrator",
};
/// Builtin System Admin account.
pub static ref BUILTIN_ACCOUNT_ADMIN: BuiltinAccount = BuiltinAccount {
account_type: AccountType::ServiceAccount,
entry_managed_by: None,
name: "admin",
uuid: UUID_ADMIN,
description: "Builtin System Admin account.",
displayname: "System Administrator",
};
pub static ref BUILTIN_ACCOUNT_ANONYMOUS_DL6: BuiltinAccount = BuiltinAccount {
account_type: AccountType::ServiceAccount,
entry_managed_by: Some(UUID_IDM_ADMINS),
name: "anonymous",
uuid: UUID_ANONYMOUS,
description: "Anonymous access account.",
displayname: "Anonymous",
};
}

View file

@ -0,0 +1,408 @@
use crate::entry::EntryInitNew;
use crate::prelude::*;
use crate::value::CredentialType;
use kanidm_proto::internal::{Filter, OperationError, UiHint};
#[derive(Clone, Debug, Default)]
/// Built-in group definitions
pub struct BuiltinGroup {
pub name: &'static str,
pub description: &'static str,
pub uuid: uuid::Uuid,
pub members: Vec<uuid::Uuid>,
pub entry_managed_by: Option<uuid::Uuid>,
pub dyngroup: bool,
pub dyngroup_filter: Option<Filter>,
pub extra_attributes: Vec<(Attribute, Value)>,
}
impl TryFrom<BuiltinGroup> for EntryInitNew {
type Error = OperationError;
fn try_from(val: BuiltinGroup) -> Result<Self, OperationError> {
let mut entry = EntryInitNew::new();
if val.uuid >= DYNAMIC_RANGE_MINIMUM_UUID {
error!("Builtin ACP has invalid UUID! {:?}", val);
return Err(OperationError::InvalidUuid);
}
entry.add_ava(Attribute::Name, Value::new_iname(val.name));
entry.add_ava(Attribute::Description, Value::new_utf8s(val.description));
// classes for groups
entry.set_ava(
Attribute::Class,
vec![EntryClass::Group.into(), EntryClass::Object.into()],
);
if val.dyngroup {
if !val.members.is_empty() {
return Err(OperationError::InvalidSchemaState(format!(
"Builtin dyngroup {} has members specified, this is not allowed",
val.name
)));
}
entry.add_ava(Attribute::Class, EntryClass::DynGroup.to_value());
match val.dyngroup_filter {
Some(filter) => entry.add_ava(Attribute::DynGroupFilter, Value::JsonFilt(filter)),
None => {
error!(
"No filter specified for dyngroup '{}' this is going to break things!",
val.name
);
return Err(OperationError::FilterGeneration);
}
};
}
if let Some(entry_manager) = val.entry_managed_by {
entry.add_ava(Attribute::EntryManagedBy, Value::Refer(entry_manager));
}
entry.add_ava(Attribute::Uuid, Value::Uuid(val.uuid));
entry.set_ava(
Attribute::Member,
val.members
.into_iter()
.map(Value::Refer)
.collect::<Vec<Value>>(),
);
// add any extra attributes
val.extra_attributes
.into_iter()
.for_each(|(attr, val)| entry.add_ava(attr, val));
// all done!
Ok(entry)
}
}
lazy_static! {
// There are our built in "roles". They encapsulate some higher level collections
// of roles. The intent is to allow a pretty generic and correct by default set
// of these use cases.
pub static ref BUILTIN_GROUP_SYSTEM_ADMINS_V1: BuiltinGroup = BuiltinGroup {
name: "system_admins",
description: "Builtin System Administrators Group.",
uuid: UUID_SYSTEM_ADMINS,
entry_managed_by: Some(UUID_SYSTEM_ADMINS),
members: vec![UUID_ADMIN],
..Default::default()
};
pub static ref BUILTIN_GROUP_IDM_ADMINS_V1: BuiltinGroup = BuiltinGroup {
name: "idm_admins",
description: "Builtin IDM Administrators Group.",
uuid: UUID_IDM_ADMINS,
entry_managed_by: Some(UUID_IDM_ADMINS),
members: vec![UUID_IDM_ADMIN],
..Default::default()
};
pub static ref BUILTIN_GROUP_SERVICE_DESK: BuiltinGroup = BuiltinGroup {
name: "idm_service_desk",
description: "Builtin Service Desk Group.",
uuid: UUID_IDM_SERVICE_DESK,
entry_managed_by: Some(UUID_IDM_ADMINS),
members: vec![],
..Default::default()
};
}
lazy_static! {
// These are the "finer" roles. They encapsulate different concepts in the system.
// The next section is the "system style" roles. These adjust the operation of
// kanidm and relate to it's internals and how it functions.
pub static ref BUILTIN_GROUP_RECYCLE_BIN_ADMINS: BuiltinGroup = BuiltinGroup {
name: "idm_recycle_bin_admins",
description: "Builtin Recycle Bin Administrators Group.",
uuid: UUID_IDM_RECYCLE_BIN_ADMINS,
entry_managed_by: Some(UUID_SYSTEM_ADMINS),
members: vec![UUID_SYSTEM_ADMINS],
..Default::default()
};
/// Builtin IDM Group for granting local domain administration rights and trust administration rights
pub static ref BUILTIN_GROUP_DOMAIN_ADMINS: BuiltinGroup = BuiltinGroup {
name: "domain_admins",
description: "Builtin IDM Group for granting local domain administration rights and trust administration rights.",
uuid: UUID_DOMAIN_ADMINS,
entry_managed_by: Some(UUID_SYSTEM_ADMINS),
members: vec![UUID_SYSTEM_ADMINS],
..Default::default()
};
pub static ref BUILTIN_GROUP_SCHEMA_ADMINS: BuiltinGroup = BuiltinGroup {
name: "idm_schema_admins",
description: "Builtin Schema Administration Group.",
uuid: UUID_IDM_SCHEMA_ADMINS,
entry_managed_by: Some(UUID_SYSTEM_ADMINS),
members: vec![UUID_SYSTEM_ADMINS],
..Default::default()
};
pub static ref BUILTIN_GROUP_ACCESS_CONTROL_ADMINS: BuiltinGroup = BuiltinGroup {
name: "idm_access_control_admins",
description: "Builtin Access Control Administration Group.",
entry_managed_by: Some(UUID_SYSTEM_ADMINS),
uuid: UUID_IDM_ACCESS_CONTROL_ADMINS,
members: vec![UUID_SYSTEM_ADMINS],
..Default::default()
};
// These are the IDM roles. They concern application integration, user permissions
// and credential security management.
/// Builtin IDM Group for managing persons and their account details
pub static ref BUILTIN_GROUP_PEOPLE_ADMINS: BuiltinGroup = BuiltinGroup {
name: "idm_people_admins",
description: "Builtin People Administration Group.",
uuid: UUID_IDM_PEOPLE_ADMINS,
entry_managed_by: Some(UUID_IDM_ADMINS),
members: vec![UUID_IDM_ADMINS],
..Default::default()
};
pub static ref BUILTIN_GROUP_PEOPLE_ON_BOARDING: BuiltinGroup = BuiltinGroup {
name: "idm_people_on_boarding",
description: "Builtin People On Boarding Group.",
uuid: UUID_IDM_PEOPLE_ON_BOARDING,
entry_managed_by: Some(UUID_IDM_ADMINS),
members: vec![],
..Default::default()
};
/// Builtin IDM Group for granting elevated people (personal data) read permissions.
pub static ref BUILTIN_GROUP_PEOPLE_PII_READ: BuiltinGroup = BuiltinGroup {
name: "idm_people_pii_read",
description: "Builtin IDM Group for granting elevated people (personal data) read permissions.",
uuid: UUID_IDM_PEOPLE_PII_READ,
entry_managed_by: Some(UUID_IDM_ADMINS),
members: vec![],
..Default::default()
};
/// Builtin IDM Group for granting people the ability to write to their own name attributes.
pub static ref BUILTIN_GROUP_PEOPLE_SELF_NAME_WRITE_DL7: BuiltinGroup = BuiltinGroup {
name: "idm_people_self_name_write",
description: "Builtin IDM Group denoting users that can write to their own name attributes.",
uuid: UUID_IDM_PEOPLE_SELF_NAME_WRITE,
entry_managed_by: Some(UUID_IDM_ADMINS),
members: vec![
UUID_IDM_ALL_PERSONS
],
..Default::default()
};
pub static ref BUILTIN_GROUP_SERVICE_ACCOUNT_ADMINS: BuiltinGroup = BuiltinGroup {
name: "idm_service_account_admins",
description: "Builtin Service Account Administration Group.",
uuid: UUID_IDM_SERVICE_ACCOUNT_ADMINS,
entry_managed_by: Some(UUID_IDM_ADMINS),
members: vec![UUID_IDM_ADMINS],
..Default::default()
};
/// Builtin IDM Group for managing oauth2 resource server integrations to this authentication domain.
pub static ref BUILTIN_GROUP_OAUTH2_ADMINS: BuiltinGroup = BuiltinGroup {
name: "idm_oauth2_admins",
description: "Builtin Oauth2 Integration Administration Group.",
uuid: UUID_IDM_OAUTH2_ADMINS,
entry_managed_by: Some(UUID_IDM_ADMINS),
members: vec![UUID_IDM_ADMINS],
..Default::default()
};
pub static ref BUILTIN_GROUP_RADIUS_SERVICE_ADMINS: BuiltinGroup = BuiltinGroup {
name: "idm_radius_service_admins",
description: "Builtin Radius Administration Group.",
uuid: UUID_IDM_RADIUS_ADMINS,
entry_managed_by: Some(UUID_IDM_ADMINS),
members: vec![UUID_IDM_ADMINS],
..Default::default()
};
/// Builtin IDM Group for RADIUS server access delegation.
pub static ref BUILTIN_IDM_RADIUS_SERVERS_V1: BuiltinGroup = BuiltinGroup {
name: "idm_radius_servers",
description: "Builtin IDM Group for RADIUS server access delegation.",
uuid: UUID_IDM_RADIUS_SERVERS,
entry_managed_by: Some(UUID_IDM_RADIUS_ADMINS),
members: vec![
],
..Default::default()
};
pub static ref BUILTIN_GROUP_MAIL_SERVICE_ADMINS_DL8: BuiltinGroup = BuiltinGroup {
name: "idm_mail_service_admins",
description: "Builtin Mail Server Administration Group.",
uuid: UUID_IDM_MAIL_ADMINS,
entry_managed_by: Some(UUID_IDM_ADMINS),
members: vec![UUID_IDM_ADMINS],
..Default::default()
};
/// Builtin IDM Group for MAIL server Access delegation.
pub static ref BUILTIN_IDM_MAIL_SERVERS_DL8: BuiltinGroup = BuiltinGroup {
name: "idm_mail_servers",
description: "Builtin IDM Group for MAIL server access delegation.",
uuid: UUID_IDM_MAIL_SERVERS,
entry_managed_by: Some(UUID_IDM_MAIL_ADMINS),
members: vec![
],
..Default::default()
};
pub static ref BUILTIN_GROUP_ACCOUNT_POLICY_ADMINS: BuiltinGroup = BuiltinGroup {
name: "idm_account_policy_admins",
description: "Builtin Account Policy Administration Group.",
uuid: UUID_IDM_ACCOUNT_POLICY_ADMINS,
entry_managed_by: Some(UUID_IDM_ADMINS),
members: vec![UUID_IDM_ADMINS],
..Default::default()
};
/// Builtin IDM Group for managing posix/unix attributes on groups and users.
pub static ref BUILTIN_GROUP_UNIX_ADMINS: BuiltinGroup = BuiltinGroup {
name: "idm_unix_admins",
description: "Builtin Unix Administration Group.",
uuid: UUID_IDM_UNIX_ADMINS,
entry_managed_by: Some(UUID_IDM_ADMINS),
members: vec![UUID_IDM_ADMINS],
..Default::default()
};
/// Builtin IDM Group for managing client authentication certificates.
pub static ref BUILTIN_GROUP_CLIENT_CERTIFICATE_ADMINS_DL7: BuiltinGroup = BuiltinGroup {
name: "idm_client_certificate_admins",
description: "Builtin Client Certificate Administration Group.",
uuid: UUID_IDM_CLIENT_CERTIFICATE_ADMINS,
entry_managed_by: Some(UUID_IDM_ADMINS),
members: vec![UUID_IDM_ADMINS],
..Default::default()
};
/// Builtin IDM Group for granting elevated group write and lifecycle permissions.
pub static ref IDM_GROUP_ADMINS_V1: BuiltinGroup = BuiltinGroup {
name: "idm_group_admins",
description: "Builtin IDM Group for granting elevated group write and lifecycle permissions.",
uuid: UUID_IDM_GROUP_ADMINS,
entry_managed_by: Some(UUID_IDM_ADMINS),
members: vec![UUID_IDM_ADMINS],
..Default::default()
};
/// Self-write of mail
pub static ref IDM_PEOPLE_SELF_MAIL_WRITE_DL7: BuiltinGroup = BuiltinGroup {
name: "idm_people_self_mail_write",
description: "Builtin IDM Group for people accounts to update their own mail.",
uuid: UUID_IDM_PEOPLE_SELF_MAIL_WRITE,
members: Vec::with_capacity(0),
..Default::default()
};
}
// at some point vs code just gives up on syntax highlighting inside lazy_static...
lazy_static! {
pub static ref IDM_ALL_PERSONS: BuiltinGroup = BuiltinGroup {
name: "idm_all_persons",
description: "Builtin IDM dynamic group containing all persons.",
uuid: UUID_IDM_ALL_PERSONS,
members: Vec::with_capacity(0),
dyngroup: true,
dyngroup_filter: Some(
Filter::And(vec![
Filter::Eq(Attribute::Class.to_string(), EntryClass::Person.to_string()),
Filter::Eq(Attribute::Class.to_string(), EntryClass::Account.to_string()),
])
),
extra_attributes: vec![
// Enable account policy by default
(Attribute::Class, EntryClass::AccountPolicy.to_value()),
// Enforce this is a system protected object
(Attribute::Class, EntryClass::System.to_value()),
// MFA By Default
(Attribute::CredentialTypeMinimum, CredentialType::Mfa.into()),
],
..Default::default()
};
pub static ref IDM_ALL_ACCOUNTS: BuiltinGroup = BuiltinGroup {
name: "idm_all_accounts",
description: "Builtin IDM dynamic group containing all entries that can authenticate.",
uuid: UUID_IDM_ALL_ACCOUNTS,
members: Vec::with_capacity(0),
dyngroup: true,
dyngroup_filter: Some(
Filter::Eq(Attribute::Class.to_string(), EntryClass::Account.to_string()),
),
extra_attributes: vec![
// Enable account policy by default
(Attribute::Class, EntryClass::AccountPolicy.to_value()),
// Enforce this is a system protected object
(Attribute::Class, EntryClass::System.to_value()),
],
..Default::default()
};
pub static ref IDM_UI_ENABLE_EXPERIMENTAL_FEATURES: BuiltinGroup = BuiltinGroup {
name: "idm_ui_enable_experimental_features",
description: "Members of this group will have access to experimental web UI features.",
uuid: UUID_IDM_UI_ENABLE_EXPERIMENTAL_FEATURES,
entry_managed_by: Some(UUID_IDM_ADMINS),
extra_attributes: vec![
(Attribute::GrantUiHint, Value::UiHint(UiHint::ExperimentalFeatures))
],
..Default::default()
};
/// Members of this group will have access to read the mail attribute of all persons and service accounts.
pub static ref IDM_ACCOUNT_MAIL_READ: BuiltinGroup = BuiltinGroup {
name: "idm_account_mail_read",
description: "Members of this group will have access to read the mail attribute of all persons and service accounts.",
entry_managed_by: Some(UUID_IDM_ACCESS_CONTROL_ADMINS),
uuid: UUID_IDM_ACCOUNT_MAIL_READ,
..Default::default()
};
/// This must be the last group to init to include the UUID of the other high priv groups.
pub static ref IDM_HIGH_PRIVILEGE_DL8: BuiltinGroup = BuiltinGroup {
name: "idm_high_privilege",
uuid: UUID_IDM_HIGH_PRIVILEGE,
entry_managed_by: Some(UUID_IDM_ACCESS_CONTROL_ADMINS),
description: "Builtin IDM provided groups with high levels of access that should be audited and limited in modification.",
members: vec![
UUID_SYSTEM_ADMINS,
UUID_IDM_ADMINS,
UUID_DOMAIN_ADMINS,
UUID_IDM_SERVICE_DESK,
UUID_IDM_RECYCLE_BIN_ADMINS,
UUID_IDM_SCHEMA_ADMINS,
UUID_IDM_ACCESS_CONTROL_ADMINS,
UUID_IDM_OAUTH2_ADMINS,
UUID_IDM_RADIUS_ADMINS,
UUID_IDM_ACCOUNT_POLICY_ADMINS,
UUID_IDM_RADIUS_SERVERS,
UUID_IDM_GROUP_ADMINS,
UUID_IDM_UNIX_ADMINS,
UUID_IDM_PEOPLE_PII_READ,
UUID_IDM_PEOPLE_ADMINS,
UUID_IDM_PEOPLE_ON_BOARDING,
UUID_IDM_SERVICE_ACCOUNT_ADMINS,
UUID_IDM_CLIENT_CERTIFICATE_ADMINS,
UUID_IDM_APPLICATION_ADMINS,
UUID_IDM_MAIL_ADMINS,
UUID_IDM_HIGH_PRIVILEGE,
],
..Default::default()
};
pub static ref BUILTIN_GROUP_APPLICATION_ADMINS_DL8: BuiltinGroup = BuiltinGroup {
name: "idm_application_admins",
uuid: UUID_IDM_APPLICATION_ADMINS,
description: "Builtin Application Administration Group.",
entry_managed_by: Some(UUID_IDM_ADMINS),
members: vec![UUID_IDM_ADMINS],
..Default::default()
};
}

View file

@ -0,0 +1,18 @@
use crate::constants::entries::{Attribute, EntryClass};
use crate::constants::uuids::UUID_KEY_PROVIDER_INTERNAL;
use crate::entry::{Entry, EntryInit, EntryInitNew, EntryNew};
use crate::value::Value;
lazy_static! {
pub static ref E_KEY_PROVIDER_INTERNAL_DL6: EntryInitNew = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::KeyProvider.to_value()),
(Attribute::Class, EntryClass::KeyProviderInternal.to_value()),
(Attribute::Uuid, Value::Uuid(UUID_KEY_PROVIDER_INTERNAL)),
(Attribute::Name, Value::new_iname("key_provider_internal")),
(
Attribute::Description,
Value::new_utf8s("The default database internal cryptographic key provider.")
)
);
}

View file

@ -0,0 +1,264 @@
mod access;
mod accounts;
mod groups;
mod key_providers;
mod schema;
mod system_config;
use self::access::*;
use self::accounts::*;
use self::groups::*;
use self::key_providers::*;
use self::schema::*;
use self::system_config::*;
use crate::prelude::EntryInitNew;
use kanidm_proto::internal::OperationError;
pub fn phase_1_schema_attrs() -> Vec<EntryInitNew> {
vec![
SCHEMA_ATTR_SYNC_CREDENTIAL_PORTAL.clone().into(),
SCHEMA_ATTR_SYNC_YIELD_AUTHORITY.clone().into(),
SCHEMA_ATTR_ACCOUNT_EXPIRE.clone().into(),
SCHEMA_ATTR_ACCOUNT_VALID_FROM.clone().into(),
SCHEMA_ATTR_API_TOKEN_SESSION.clone().into(),
SCHEMA_ATTR_AUTH_SESSION_EXPIRY.clone().into(),
SCHEMA_ATTR_AUTH_PRIVILEGE_EXPIRY.clone().into(),
SCHEMA_ATTR_AUTH_PASSWORD_MINIMUM_LENGTH.clone().into(),
SCHEMA_ATTR_BADLIST_PASSWORD.clone().into(),
SCHEMA_ATTR_CREDENTIAL_UPDATE_INTENT_TOKEN.clone().into(),
SCHEMA_ATTR_ATTESTED_PASSKEYS.clone().into(),
SCHEMA_ATTR_DOMAIN_DISPLAY_NAME.clone().into(),
SCHEMA_ATTR_DOMAIN_LDAP_BASEDN.clone().into(),
SCHEMA_ATTR_DOMAIN_NAME.clone().into(),
SCHEMA_ATTR_LDAP_ALLOW_UNIX_PW_BIND.clone().into(),
SCHEMA_ATTR_DOMAIN_SSID.clone().into(),
SCHEMA_ATTR_DOMAIN_TOKEN_KEY.clone().into(),
SCHEMA_ATTR_DOMAIN_UUID.clone().into(),
SCHEMA_ATTR_DYNGROUP_FILTER.clone().into(),
SCHEMA_ATTR_EC_KEY_PRIVATE.clone().into(),
SCHEMA_ATTR_ES256_PRIVATE_KEY_DER.clone().into(),
SCHEMA_ATTR_FERNET_PRIVATE_KEY_STR.clone().into(),
SCHEMA_ATTR_GIDNUMBER.clone().into(),
SCHEMA_ATTR_GRANT_UI_HINT.clone().into(),
SCHEMA_ATTR_JWS_ES256_PRIVATE_KEY.clone().into(),
SCHEMA_ATTR_LOGINSHELL.clone().into(),
SCHEMA_ATTR_NAME_HISTORY.clone().into(),
SCHEMA_ATTR_NSUNIQUEID.clone().into(),
SCHEMA_ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE
.clone()
.into(),
SCHEMA_ATTR_OAUTH2_CONSENT_SCOPE_MAP.clone().into(),
SCHEMA_ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE.clone().into(),
SCHEMA_ATTR_OAUTH2_PREFER_SHORT_USERNAME.clone().into(),
SCHEMA_ATTR_OAUTH2_RS_BASIC_SECRET.clone().into(),
SCHEMA_ATTR_OAUTH2_RS_IMPLICIT_SCOPES.clone().into(),
SCHEMA_ATTR_OAUTH2_RS_NAME.clone().into(),
SCHEMA_ATTR_OAUTH2_RS_ORIGIN_LANDING.clone().into(),
SCHEMA_ATTR_OAUTH2_RS_SCOPE_MAP.clone().into(),
SCHEMA_ATTR_OAUTH2_RS_SUP_SCOPE_MAP.clone().into(),
SCHEMA_ATTR_OAUTH2_RS_TOKEN_KEY.clone().into(),
SCHEMA_ATTR_OAUTH2_SESSION.clone().into(),
SCHEMA_ATTR_PASSKEYS.clone().into(),
SCHEMA_ATTR_PRIMARY_CREDENTIAL.clone().into(),
SCHEMA_ATTR_PRIVATE_COOKIE_KEY.clone().into(),
SCHEMA_ATTR_RADIUS_SECRET.clone().into(),
SCHEMA_ATTR_RS256_PRIVATE_KEY_DER.clone().into(),
SCHEMA_ATTR_SSH_PUBLICKEY.clone().into(),
SCHEMA_ATTR_SYNC_COOKIE.clone().into(),
SCHEMA_ATTR_SYNC_TOKEN_SESSION.clone().into(),
SCHEMA_ATTR_UNIX_PASSWORD.clone().into(),
SCHEMA_ATTR_USER_AUTH_TOKEN_SESSION.clone().into(),
SCHEMA_ATTR_DENIED_NAME.clone().into(),
SCHEMA_ATTR_CREDENTIAL_TYPE_MINIMUM.clone().into(),
SCHEMA_ATTR_WEBAUTHN_ATTESTATION_CA_LIST.clone().into(),
// DL4
SCHEMA_ATTR_OAUTH2_RS_CLAIM_MAP_DL4.clone().into(),
SCHEMA_ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT_DL4
.clone()
.into(),
// DL5
// DL6
SCHEMA_ATTR_LIMIT_SEARCH_MAX_RESULTS_DL6.clone().into(),
SCHEMA_ATTR_LIMIT_SEARCH_MAX_FILTER_TEST_DL6.clone().into(),
SCHEMA_ATTR_KEY_INTERNAL_DATA_DL6.clone().into(),
SCHEMA_ATTR_KEY_PROVIDER_DL6.clone().into(),
SCHEMA_ATTR_KEY_ACTION_ROTATE_DL6.clone().into(),
SCHEMA_ATTR_KEY_ACTION_REVOKE_DL6.clone().into(),
SCHEMA_ATTR_KEY_ACTION_IMPORT_JWS_ES256_DL6.clone().into(),
// DL7
SCHEMA_ATTR_PATCH_LEVEL_DL7.clone().into(),
SCHEMA_ATTR_DOMAIN_DEVELOPMENT_TAINT_DL7.clone().into(),
SCHEMA_ATTR_REFERS_DL7.clone().into(),
SCHEMA_ATTR_CERTIFICATE_DL7.clone().into(),
SCHEMA_ATTR_OAUTH2_RS_ORIGIN_DL7.clone().into(),
SCHEMA_ATTR_OAUTH2_STRICT_REDIRECT_URI_DL7.clone().into(),
SCHEMA_ATTR_MAIL_DL7.clone().into(),
SCHEMA_ATTR_LEGALNAME_DL7.clone().into(),
SCHEMA_ATTR_DISPLAYNAME_DL7.clone().into(),
// DL8
SCHEMA_ATTR_LINKED_GROUP_DL8.clone().into(),
SCHEMA_ATTR_APPLICATION_PASSWORD_DL8.clone().into(),
SCHEMA_ATTR_ALLOW_PRIMARY_CRED_FALLBACK_DL8.clone().into(),
// DL9
SCHEMA_ATTR_OAUTH2_DEVICE_FLOW_ENABLE_DL9.clone().into(),
SCHEMA_ATTR_DOMAIN_ALLOW_EASTER_EGGS_DL9.clone().into(),
]
}
pub fn phase_2_schema_classes() -> Vec<EntryInitNew> {
vec![
SCHEMA_CLASS_DYNGROUP.clone().into(),
SCHEMA_CLASS_ORGPERSON.clone().into(),
SCHEMA_CLASS_POSIXACCOUNT.clone().into(),
SCHEMA_CLASS_POSIXGROUP.clone().into(),
SCHEMA_CLASS_SYSTEM_CONFIG.clone().into(),
// DL4
SCHEMA_CLASS_OAUTH2_RS_PUBLIC_DL4.clone().into(),
// DL5
SCHEMA_CLASS_ACCOUNT_DL5.clone().into(),
SCHEMA_CLASS_OAUTH2_RS_BASIC_DL5.clone().into(),
// DL6
SCHEMA_CLASS_GROUP_DL6.clone().into(),
SCHEMA_CLASS_KEY_PROVIDER_DL6.clone().into(),
SCHEMA_CLASS_KEY_PROVIDER_INTERNAL_DL6.clone().into(),
SCHEMA_CLASS_KEY_OBJECT_DL6.clone().into(),
SCHEMA_CLASS_KEY_OBJECT_JWT_ES256_DL6.clone().into(),
SCHEMA_CLASS_KEY_OBJECT_JWE_A128GCM_DL6.clone().into(),
SCHEMA_CLASS_KEY_OBJECT_INTERNAL_DL6.clone().into(),
// DL7
SCHEMA_CLASS_SERVICE_ACCOUNT_DL7.clone().into(),
SCHEMA_CLASS_SYNC_ACCOUNT_DL7.clone().into(),
SCHEMA_CLASS_CLIENT_CERTIFICATE_DL7.clone().into(),
// DL8
SCHEMA_CLASS_ACCOUNT_POLICY_DL8.clone().into(),
SCHEMA_CLASS_APPLICATION_DL8.clone().into(),
SCHEMA_CLASS_PERSON_DL8.clone().into(),
// DL9
SCHEMA_CLASS_OAUTH2_RS_DL9.clone().into(),
SCHEMA_CLASS_DOMAIN_INFO_DL9.clone().into(),
]
}
pub fn phase_3_key_provider() -> Vec<EntryInitNew> {
vec![E_KEY_PROVIDER_INTERNAL_DL6.clone()]
}
pub fn phase_4_system_entries() -> Vec<EntryInitNew> {
vec![
E_SYSTEM_INFO_V1.clone(),
E_DOMAIN_INFO_DL6.clone(),
E_SYSTEM_CONFIG_V1.clone(),
]
}
pub fn phase_5_builtin_admin_entries() -> Result<Vec<EntryInitNew>, OperationError> {
Ok(vec![
BUILTIN_ACCOUNT_ADMIN.clone().into(),
BUILTIN_ACCOUNT_IDM_ADMIN.clone().into(),
BUILTIN_GROUP_SYSTEM_ADMINS_V1.clone().try_into()?,
BUILTIN_GROUP_IDM_ADMINS_V1.clone().try_into()?,
// We need to push anonymous *after* groups due to entry-managed-by
BUILTIN_ACCOUNT_ANONYMOUS_DL6.clone().into(),
])
}
pub fn phase_6_builtin_non_admin_entries() -> Result<Vec<EntryInitNew>, OperationError> {
Ok(vec![
BUILTIN_GROUP_DOMAIN_ADMINS.clone().try_into()?,
BUILTIN_GROUP_SCHEMA_ADMINS.clone().try_into()?,
BUILTIN_GROUP_ACCESS_CONTROL_ADMINS.clone().try_into()?,
BUILTIN_GROUP_UNIX_ADMINS.clone().try_into()?,
BUILTIN_GROUP_RECYCLE_BIN_ADMINS.clone().try_into()?,
BUILTIN_GROUP_SERVICE_DESK.clone().try_into()?,
BUILTIN_GROUP_OAUTH2_ADMINS.clone().try_into()?,
BUILTIN_GROUP_RADIUS_SERVICE_ADMINS.clone().try_into()?,
BUILTIN_GROUP_ACCOUNT_POLICY_ADMINS.clone().try_into()?,
BUILTIN_GROUP_PEOPLE_ADMINS.clone().try_into()?,
BUILTIN_GROUP_PEOPLE_PII_READ.clone().try_into()?,
BUILTIN_GROUP_PEOPLE_ON_BOARDING.clone().try_into()?,
BUILTIN_GROUP_SERVICE_ACCOUNT_ADMINS.clone().try_into()?,
BUILTIN_GROUP_MAIL_SERVICE_ADMINS_DL8.clone().try_into()?,
IDM_GROUP_ADMINS_V1.clone().try_into()?,
IDM_ALL_PERSONS.clone().try_into()?,
IDM_ALL_ACCOUNTS.clone().try_into()?,
BUILTIN_IDM_RADIUS_SERVERS_V1.clone().try_into()?,
BUILTIN_IDM_MAIL_SERVERS_DL8.clone().try_into()?,
BUILTIN_GROUP_PEOPLE_SELF_NAME_WRITE_DL7
.clone()
.try_into()?,
IDM_PEOPLE_SELF_MAIL_WRITE_DL7.clone().try_into()?,
BUILTIN_GROUP_CLIENT_CERTIFICATE_ADMINS_DL7
.clone()
.try_into()?,
BUILTIN_GROUP_APPLICATION_ADMINS_DL8.clone().try_into()?,
// Write deps on read.clone().try_into()?, so write must be added first.
// All members must exist before we write HP
IDM_HIGH_PRIVILEGE_DL8.clone().try_into()?,
// other things
IDM_UI_ENABLE_EXPERIMENTAL_FEATURES.clone().try_into()?,
IDM_ACCOUNT_MAIL_READ.clone().try_into()?,
])
}
pub fn phase_7_builtin_access_control_profiles() -> Vec<EntryInitNew> {
vec![
// Built in access controls.
IDM_ACP_RECYCLE_BIN_SEARCH_V1.clone().into(),
IDM_ACP_RECYCLE_BIN_REVIVE_V1.clone().into(),
IDM_ACP_SCHEMA_WRITE_ATTRS_V1.clone().into(),
IDM_ACP_SCHEMA_WRITE_CLASSES_V1.clone().into(),
IDM_ACP_ACP_MANAGE_V1.clone().into(),
IDM_ACP_GROUP_ENTRY_MANAGED_BY_MODIFY_V1.clone().into(),
IDM_ACP_GROUP_ENTRY_MANAGER_V1.clone().into(),
IDM_ACP_SYNC_ACCOUNT_MANAGE_V1.clone().into(),
IDM_ACP_RADIUS_SERVERS_V1.clone().into(),
IDM_ACP_RADIUS_SECRET_MANAGE_V1.clone().into(),
IDM_ACP_PEOPLE_SELF_WRITE_MAIL_V1.clone().into(),
IDM_ACP_ACCOUNT_SELF_WRITE_V1.clone().into(),
IDM_ACP_ALL_ACCOUNTS_POSIX_READ_V1.clone().into(),
IDM_ACP_SYSTEM_CONFIG_ACCOUNT_POLICY_MANAGE_V1
.clone()
.into(),
IDM_ACP_GROUP_UNIX_MANAGE_V1.clone().into(),
IDM_ACP_HP_GROUP_UNIX_MANAGE_V1.clone().into(),
IDM_ACP_GROUP_READ_V1.clone().into(),
IDM_ACP_ACCOUNT_UNIX_EXTEND_V1.clone().into(),
IDM_ACP_PEOPLE_PII_READ_V1.clone().into(),
IDM_ACP_PEOPLE_PII_MANAGE_V1.clone().into(),
IDM_ACP_PEOPLE_READ_V1.clone().into(),
IDM_ACP_PEOPLE_MANAGE_V1.clone().into(),
IDM_ACP_PEOPLE_DELETE_V1.clone().into(),
IDM_ACP_PEOPLE_CREDENTIAL_RESET_V1.clone().into(),
IDM_ACP_HP_PEOPLE_CREDENTIAL_RESET_V1.clone().into(),
IDM_ACP_SERVICE_ACCOUNT_CREATE_V1.clone().into(),
IDM_ACP_SERVICE_ACCOUNT_DELETE_V1.clone().into(),
IDM_ACP_SERVICE_ACCOUNT_ENTRY_MANAGER_V1.clone().into(),
IDM_ACP_SERVICE_ACCOUNT_ENTRY_MANAGED_BY_MODIFY_V1
.clone()
.into(),
IDM_ACP_HP_SERVICE_ACCOUNT_ENTRY_MANAGED_BY_MODIFY_V1
.clone()
.into(),
IDM_ACP_SERVICE_ACCOUNT_MANAGE_V1.clone().into(),
// DL4
// DL5
// DL6
IDM_ACP_PEOPLE_CREATE_DL6.clone().into(),
IDM_ACP_ACCOUNT_MAIL_READ_DL6.clone().into(),
// DL7
IDM_ACP_SELF_NAME_WRITE_DL7.clone().into(),
IDM_ACP_HP_CLIENT_CERTIFICATE_MANAGER_DL7.clone().into(),
// DL8
IDM_ACP_SELF_READ_DL8.clone().into(),
IDM_ACP_SELF_WRITE_DL8.clone().into(),
IDM_ACP_APPLICATION_MANAGE_DL8.clone().into(),
IDM_ACP_APPLICATION_ENTRY_MANAGER_DL8.clone().into(),
IDM_ACP_MAIL_SERVERS_DL8.clone().into(),
IDM_ACP_GROUP_ACCOUNT_POLICY_MANAGE_DL8.clone().into(),
// DL9
IDM_ACP_OAUTH2_MANAGE_DL9.clone().into(),
IDM_ACP_GROUP_MANAGE_DL9.clone().into(),
IDM_ACP_DOMAIN_ADMIN_DL9.clone().into(),
]
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,24 @@
pub(crate) mod dl10;
pub(crate) mod dl8;
pub(crate) mod dl9;
mod types;
#[cfg(test)]
pub(crate) use dl10::accounts::BUILTIN_ACCOUNT_ANONYMOUS_DL6 as BUILTIN_ACCOUNT_ANONYMOUS;
#[cfg(test)]
use self::types::BuiltinAccount;
#[cfg(test)]
lazy_static! {
/// Builtin System Admin account.
pub static ref BUILTIN_ACCOUNT_TEST_PERSON: BuiltinAccount = BuiltinAccount {
account_type: kanidm_proto::v1::AccountType::Person,
entry_managed_by: None,
name: "test_person",
uuid: crate::constants::uuids::UUID_TESTPERSON_1,
description: "Test Person",
displayname: "Test Person",
};
}

View file

@ -0,0 +1,83 @@
//! Constant Entries for the IDM
use crate::constants::uuids::*;
use crate::entry::EntryInitNew;
use crate::prelude::EntryClass;
use crate::value::Value;
pub use kanidm_proto::attribute::Attribute;
use kanidm_proto::v1::AccountType;
use uuid::Uuid;
#[derive(Debug, Clone)]
/// Built in accounts such as anonymous, idm_admin and admin
pub struct BuiltinAccount {
pub account_type: kanidm_proto::v1::AccountType,
pub entry_managed_by: Option<uuid::Uuid>,
pub name: &'static str,
pub uuid: Uuid,
pub description: &'static str,
pub displayname: &'static str,
}
#[cfg(test)]
impl Default for BuiltinAccount {
fn default() -> Self {
BuiltinAccount {
account_type: AccountType::ServiceAccount,
entry_managed_by: None,
name: "",
uuid: Uuid::new_v4(),
description: "<set description>",
displayname: "<set displayname>",
}
}
}
#[cfg(test)]
impl From<BuiltinAccount> for crate::idm::account::Account {
fn from(value: BuiltinAccount) -> Self {
Self {
name: value.name.to_string(),
uuid: value.uuid,
displayname: value.displayname.to_string(),
spn: format!("{}@example.com", value.name),
mail_primary: None,
mail: Vec::with_capacity(0),
..Default::default()
}
}
}
impl From<BuiltinAccount> for EntryInitNew {
fn from(value: BuiltinAccount) -> Self {
let mut entry = EntryInitNew::new();
entry.add_ava(Attribute::Name, Value::new_iname(value.name));
#[allow(clippy::panic)]
if value.uuid >= DYNAMIC_RANGE_MINIMUM_UUID {
panic!("Builtin ACP has invalid UUID! {:?}", value);
}
entry.add_ava(Attribute::Uuid, Value::Uuid(value.uuid));
entry.add_ava(Attribute::Description, Value::new_utf8s(value.description));
entry.add_ava(Attribute::DisplayName, Value::new_utf8s(value.displayname));
if let Some(entry_manager) = value.entry_managed_by {
entry.add_ava(Attribute::EntryManagedBy, Value::Refer(entry_manager));
}
entry.set_ava(
Attribute::Class,
vec![
EntryClass::Account.to_value(),
EntryClass::MemberOf.to_value(),
EntryClass::Object.to_value(),
],
);
match value.account_type {
AccountType::Person => entry.add_ava(Attribute::Class, EntryClass::Person.to_value()),
AccountType::ServiceAccount => {
entry.add_ava(Attribute::Class, EntryClass::ServiceAccount.to_value())
}
}
entry
}
}

View file

@ -25,6 +25,7 @@ lazy_static! {
// modification of some domain info types for local configuratiomn.
Attribute::DomainSsid,
Attribute::DomainLdapBasedn,
Attribute::LdapMaxQueryableAttrs,
Attribute::LdapAllowUnixPwBind,
Attribute::FernetPrivateKeyStr,
Attribute::Es256PrivateKeyDer,

View file

@ -10,7 +10,7 @@ impl Plugin for ValueDeny {
"plugin_value_deny"
}
#[instrument(level = "debug", name = "base_pre_create_transform", skip_all)]
#[instrument(level = "debug", name = "denied_names_pre_create_transform", skip_all)]
#[allow(clippy::cognitive_complexity)]
fn pre_create_transform(
qs: &mut QueryServerWriteTransaction,
@ -19,9 +19,25 @@ impl Plugin for ValueDeny {
) -> Result<(), OperationError> {
let denied_names = qs.denied_names();
if denied_names.is_empty() {
// Nothing to check.
return Ok(());
}
let mut pass = true;
for entry in cand {
// If the entry doesn't have a uuid, it's invalid anyway and will fail schema.
if let Some(e_uuid) = entry.get_uuid() {
// SAFETY - Thanks to JpWarren blowing his nipper clean off, we need to
// assert that the break glass and system accounts are NOT subject to
// this process.
if e_uuid < DYNAMIC_RANGE_MINIMUM_UUID {
// These entries are exempt
continue;
}
}
if let Some(name) = entry.get_ava_single_iname(Attribute::Name) {
if denied_names.contains(name) {
pass = false;
@ -37,27 +53,24 @@ impl Plugin for ValueDeny {
}
}
#[instrument(level = "debug", name = "base_pre_modify", skip_all)]
fn pre_modify(
qs: &mut QueryServerWriteTransaction,
_pre_cand: &[Arc<EntrySealedCommitted>],
pre_cand: &[Arc<EntrySealedCommitted>],
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
_me: &ModifyEvent,
) -> Result<(), OperationError> {
Self::modify(qs, cand)
Self::modify(qs, pre_cand, cand)
}
#[instrument(level = "debug", name = "base_pre_modify", skip_all)]
fn pre_batch_modify(
qs: &mut QueryServerWriteTransaction,
_pre_cand: &[Arc<EntrySealedCommitted>],
pre_cand: &[Arc<EntrySealedCommitted>],
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
_me: &BatchModifyEvent,
) -> Result<(), OperationError> {
Self::modify(qs, cand)
Self::modify(qs, pre_cand, cand)
}
#[instrument(level = "debug", name = "base::verify", skip_all)]
fn verify(qs: &mut QueryServerReadTransaction) -> Vec<Result<(), ConsistencyError>> {
let denied_names = qs.denied_names().clone();
@ -68,7 +81,15 @@ impl Plugin for ValueDeny {
match qs.internal_search(filt) {
Ok(entries) => {
for entry in entries {
results.push(Err(ConsistencyError::DeniedName(entry.get_uuid())));
let e_uuid = entry.get_uuid();
// SAFETY - Thanks to JpWarren blowing his nipper clean off, we need to
// assert that the break glass accounts are NOT subject to this process.
if e_uuid < DYNAMIC_RANGE_MINIMUM_UUID {
// These entries are exempt
continue;
}
results.push(Err(ConsistencyError::DeniedName(e_uuid)));
}
}
Err(err) => {
@ -83,17 +104,37 @@ impl Plugin for ValueDeny {
}
impl ValueDeny {
#[instrument(level = "debug", name = "denied_names_modify", skip_all)]
fn modify(
qs: &mut QueryServerWriteTransaction,
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
pre_cand: &[Arc<EntrySealedCommitted>],
cand: &mut [EntryInvalidCommitted],
) -> Result<(), OperationError> {
let denied_names = qs.denied_names();
if denied_names.is_empty() {
// Nothing to check.
return Ok(());
}
let mut pass = true;
for entry in cand {
if let Some(name) = entry.get_ava_single_iname(Attribute::Name) {
if denied_names.contains(name) {
for (pre_entry, post_entry) in pre_cand.iter().zip(cand.iter()) {
// If the entry doesn't have a uuid, it's invalid anyway and will fail schema.
let e_uuid = pre_entry.get_uuid();
// SAFETY - Thanks to JpWarren blowing his nipper clean off, we need to
// assert that the break glass accounts are NOT subject to this process.
if e_uuid < DYNAMIC_RANGE_MINIMUM_UUID {
// These entries are exempt
continue;
}
let pre_name = pre_entry.get_ava_single_iname(Attribute::Name);
let post_name = post_entry.get_ava_single_iname(Attribute::Name);
if let Some(name) = post_name {
// Only if the name is changing, and is denied.
if pre_name != post_name && denied_names.contains(name) {
pass = false;
error!(?name, "name denied by system configuration");
}
@ -117,10 +158,10 @@ mod tests {
let me_inv_m = ModifyEvent::new_internal_invalid(
filter!(f_eq(Attribute::Uuid, PVUUID_SYSTEM_CONFIG.clone())),
ModifyList::new_list(vec![Modify::Present(
Attribute::DeniedName,
Value::new_iname("tobias"),
)]),
ModifyList::new_list(vec![
Modify::Present(Attribute::DeniedName, Value::new_iname("tobias")),
Modify::Present(Attribute::DeniedName, Value::new_iname("ellie")),
]),
);
assert!(server_txn.modify(&me_inv_m).is_ok());
@ -148,30 +189,103 @@ mod tests {
#[qs_test]
async fn test_valuedeny_modify(server: &QueryServer) {
setup_name_deny(server).await;
// Create an entry that has a name which will become denied to test how it
// interacts.
let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
let t_uuid = Uuid::new_v4();
let e_uuid = Uuid::new_v4();
assert!(server_txn
.internal_create(vec![entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("newname")),
(Attribute::Uuid, Value::Uuid(t_uuid)),
(Attribute::Description, Value::new_utf8s("Tobias")),
(Attribute::DisplayName, Value::new_utf8s("Tobias"))
(Attribute::Name, Value::new_iname("ellie")),
(Attribute::Uuid, Value::Uuid(e_uuid)),
(Attribute::Description, Value::new_utf8s("Ellie Meow")),
(Attribute::DisplayName, Value::new_utf8s("Ellie Meow"))
),])
.is_ok());
// Now mod it
assert!(server_txn.commit().is_ok());
setup_name_deny(server).await;
let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
// Attempt to mod ellie.
// Can mod a different attribute
assert!(server_txn
.internal_modify_uuid(
t_uuid,
e_uuid,
&ModifyList::new_purge_and_set(Attribute::DisplayName, Value::new_utf8s("tobias"))
)
.is_ok());
// Can't mod to another invalid name.
assert!(server_txn
.internal_modify_uuid(
e_uuid,
&ModifyList::new_purge_and_set(Attribute::Name, Value::new_iname("tobias"))
)
.is_err());
// Can mod to a valid name.
assert!(server_txn
.internal_modify_uuid(
e_uuid,
&ModifyList::new_purge_and_set(
Attribute::Name,
Value::new_iname("miss_meowington")
)
)
.is_ok());
// Now mod from the valid name to an invalid one.
assert!(server_txn
.internal_modify_uuid(
e_uuid,
&ModifyList::new_purge_and_set(Attribute::Name, Value::new_iname("tobias"))
)
.is_err());
assert!(server_txn.commit().is_ok());
}
#[qs_test]
async fn test_valuedeny_jpwarren_special(server: &QueryServer) {
// Assert that our break glass accounts are exempt from this processing.
let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
let me_inv_m = ModifyEvent::new_internal_invalid(
filter!(f_eq(Attribute::Uuid, PVUUID_SYSTEM_CONFIG.clone())),
ModifyList::new_list(vec![
Modify::Present(Attribute::DeniedName, Value::new_iname("admin")),
Modify::Present(Attribute::DeniedName, Value::new_iname("idm_admin")),
]),
);
assert!(server_txn.modify(&me_inv_m).is_ok());
assert!(server_txn.commit().is_ok());
let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
assert!(server_txn
.internal_modify_uuid(
UUID_IDM_ADMIN,
&ModifyList::new_purge_and_set(
Attribute::DisplayName,
Value::new_utf8s("Idm Admin")
)
)
.is_ok());
assert!(server_txn
.internal_modify_uuid(
UUID_ADMIN,
&ModifyList::new_purge_and_set(Attribute::DisplayName, Value::new_utf8s("Admin"))
)
.is_ok());
assert!(server_txn.commit().is_ok());
}
#[qs_test]

View file

@ -1159,6 +1159,7 @@ mod tests {
},
Access, AccessClass, AccessControls, AccessControlsTransaction, AccessEffectivePermission,
};
use crate::migration_data::BUILTIN_ACCOUNT_ANONYMOUS;
use crate::prelude::*;
use crate::valueset::ValueSetIname;
@ -2511,6 +2512,7 @@ mod tests {
#[test]
fn test_access_enforce_scope_delete() {
sketching::test_init();
let ev1 = E_TESTPERSON_1.clone().into_sealed_committed();
let r_set = vec![Arc::new(ev1)];
@ -3052,7 +3054,7 @@ mod tests {
test_acp_search_reduce!(&se_a, vec![], r_set.clone(), ex_a_reduced);
// Check that anonymous is denied even though it's a member of the group.
let anon: EntryInitNew = BUILTIN_ACCOUNT_ANONYMOUS_DL6.clone().into();
let anon: EntryInitNew = BUILTIN_ACCOUNT_ANONYMOUS.clone().into();
let mut anon = anon.into_invalid_new();
anon.set_ava_set(&Attribute::MemberOf, ValueSetRefer::new(UUID_TEST_GROUP_1));
@ -3352,7 +3354,7 @@ mod tests {
#[test]
fn test_access_delete_protect_system_ranges() {
let ev1: EntryInitNew = BUILTIN_ACCOUNT_ANONYMOUS_DL6.clone().into();
let ev1: EntryInitNew = BUILTIN_ACCOUNT_ANONYMOUS.clone().into();
let ev1 = ev1.into_sealed_committed();
let r_set = vec![Arc::new(ev1)];

View file

@ -1,5 +1,6 @@
use crate::prelude::*;
use crate::migration_data;
use kanidm_proto::internal::{
DomainUpgradeCheckItem as ProtoDomainUpgradeCheckItem,
DomainUpgradeCheckReport as ProtoDomainUpgradeCheckReport,
@ -48,17 +49,24 @@ impl QueryServer {
debug!(?db_domain_version, "Before setting internal domain info");
if db_domain_version == 0 {
// No domain info was present, so neither was the rest of the IDM. We need to bootstrap
// the base-schema here.
write_txn.initialise_schema_idm()?;
// This is here to catch when we increase domain levels but didn't create the migration
// hooks. If this fails it probably means you need to add another migration hook
// in the above.
debug_assert!(domain_target_level <= DOMAIN_MAX_LEVEL);
write_txn.reload()?;
// Since we just loaded in a ton of schema, lets reindex it to make
// sure that some base IDM operations are fast. Since this is still
// very early in the bootstrap process, and very few entries exist,
// reindexing is very fast here.
write_txn.reindex(false)?;
// No domain info was present, so neither was the rest of the IDM. Bring up the
// full IDM here.
match domain_target_level {
DOMAIN_LEVEL_8 => write_txn.migrate_domain_7_to_8()?,
DOMAIN_LEVEL_9 => write_txn.migrate_domain_8_to_9()?,
DOMAIN_LEVEL_10 => write_txn.migrate_domain_9_to_10()?,
DOMAIN_LEVEL_11 => write_txn.migrate_domain_10_to_11()?,
_ => {
error!("Invalid requested domain target level for server bootstrap");
debug_assert!(false);
return Err(OperationError::MG0009InvalidTargetLevelForBootstrap);
}
}
} else {
// Domain info was present, so we need to reflect that in our server
// domain structures. If we don't do this, the in memory domain level
@ -71,23 +79,11 @@ impl QueryServer {
write_txn.force_domain_reload();
write_txn.reload()?;
}
// Indicate the schema is now ready, which allows dyngroups to work when they
// are created in the next phase of migrations.
write_txn.set_phase(ServerPhase::SchemaReady);
// Indicate the schema is now ready, which allows dyngroups to work when they
// are created in the next phase of migrations.
write_txn.set_phase(ServerPhase::SchemaReady);
// No domain info was present, so neither was the rest of the IDM. We need to bootstrap
// the base entries here.
if db_domain_version == 0 {
// Init idm will now set the system config version and minimum domain
// level if none was present
write_txn.initialise_domain_info()?;
// In this path because we create the dyn groups they are immediately added to the
// dyngroup cache and begin to operate.
write_txn.initialise_idm()?;
} else {
// #2756 - if we *aren't* creating the base IDM entries, then we
// need to force dyn groups to reload since we're now at schema
// ready. This is done indirectly by ... reloading the schema again.
@ -97,13 +93,13 @@ impl QueryServer {
// itself or a change to schema reloading. Since we aren't changing the
// dyngroup here, we have to go via the schema reload path.
write_txn.force_schema_reload();
};
// Reload as init idm affects access controls.
write_txn.reload()?;
// Reload as init idm affects access controls.
write_txn.reload()?;
// Domain info is now ready and reloaded, we can proceed.
write_txn.set_phase(ServerPhase::DomainInfoReady);
// Domain info is now ready and reloaded, we can proceed.
write_txn.set_phase(ServerPhase::DomainInfoReady);
}
// This is the start of domain info related migrations which we will need in future
// to handle replication. Due to the access control rework, and the addition of "managed by"
@ -126,20 +122,18 @@ impl QueryServer {
// If the database domain info is a lower version than our target level, we reload.
if domain_info_version < domain_target_level {
write_txn
.internal_modify_uuid(
UUID_DOMAIN_INFO,
&ModifyList::new_purge_and_set(
Attribute::Version,
Value::new_uint32(domain_target_level),
),
)
.internal_apply_domain_migration(domain_target_level)
.map(|()| {
warn!("Domain level has been raised to {}", domain_target_level);
})?;
// Reload if anything in migrations requires it - this triggers the domain migrations
// which in turn can trigger schema reloads etc.
reload_required = true;
// which in turn can trigger schema reloads etc. If the server was just brought up
// then we don't need the extra reload since we are already at the correct
// version of the server, and this call to set the target level is just for persistance
// of the value.
if domain_info_version != 0 {
reload_required = true;
}
} else if domain_development_taint {
// This forces pre-release versions to re-migrate each start up. This solves
// the domain-version-sprawl issue so that during a development cycle we can
@ -182,9 +176,6 @@ impl QueryServer {
// we would have skipped the patch level fix which needs to have occurred *first*.
if reload_required {
write_txn.reload()?;
// We are not yet at the schema phase where reindexes will auto-trigger
// so if one was required, do it now.
write_txn.reindex(false)?;
}
// Now set the db/domain devel taint flag to match our current release status
@ -206,7 +197,8 @@ impl QueryServer {
// We are ready to run
write_txn.set_phase(ServerPhase::Running);
// Commit all changes, this also triggers the reload.
// Commit all changes, this also triggers the final reload, this should be a no-op
// since we already did all the needed loads above.
write_txn.commit()?;
debug!("Database version check and migrations success! ☀️ ");
@ -217,7 +209,6 @@ impl QueryServer {
impl QueryServerWriteTransaction<'_> {
/// Apply a domain migration `to_level`. Panics if `to_level` is not greater than the active
/// level.
#[cfg(test)]
pub(crate) fn internal_apply_domain_migration(
&mut self,
to_level: u32,
@ -230,6 +221,23 @@ impl QueryServerWriteTransaction<'_> {
.and_then(|()| self.reload())
}
fn internal_migrate_or_create_batch(
&mut self,
msg: &str,
entries: Vec<EntryInitNew>,
) -> Result<(), OperationError> {
let r: Result<(), _> = entries
.into_iter()
.try_for_each(|entry| self.internal_migrate_or_create(entry));
if let Err(err) = r {
error!(?err, msg);
debug_assert!(false);
}
Ok(())
}
#[instrument(level = "debug", skip_all)]
/// - If the thing exists:
/// - Ensure the set of attributes match and are present
@ -240,7 +248,7 @@ impl QueryServerWriteTransaction<'_> {
/// This will extra classes an attributes alone!
///
/// NOTE: `gen_modlist*` IS schema aware and will handle multivalue correctly!
pub fn internal_migrate_or_create(
fn internal_migrate_or_create(
&mut self,
e: Entry<EntryInit, EntryNew>,
) -> Result<(), OperationError> {
@ -251,7 +259,7 @@ impl QueryServerWriteTransaction<'_> {
/// list of attributes, so that if an admin has modified those values then we don't
/// stomp them.
#[instrument(level = "trace", skip_all)]
pub fn internal_migrate_or_create_ignore_attrs(
fn internal_migrate_or_create_ignore_attrs(
&mut self,
mut e: Entry<EntryInit, EntryNew>,
attrs: &[Attribute],
@ -294,6 +302,75 @@ impl QueryServerWriteTransaction<'_> {
}
}
/// Migration domain level 7 to 8 (1.4.0)
#[instrument(level = "info", skip_all)]
pub(crate) fn migrate_domain_7_to_8(&mut self) -> Result<(), OperationError> {
if !cfg!(test) && DOMAIN_TGT_LEVEL < DOMAIN_LEVEL_9 {
error!("Unable to raise domain level from 8 to 9.");
return Err(OperationError::MG0004DomainLevelInDevelopment);
}
// =========== Apply changes ==============
self.internal_migrate_or_create_batch(
"phase 1 - schema attrs",
migration_data::dl8::phase_1_schema_attrs(),
)?;
self.internal_migrate_or_create_batch(
"phase 2 - schema classes",
migration_data::dl8::phase_2_schema_classes(),
)?;
// Reload for the new schema.
self.reload()?;
// Reindex?
self.reindex(false)?;
// Set Phase
self.set_phase(ServerPhase::SchemaReady);
self.internal_migrate_or_create_batch(
"phase 3 - key provider",
migration_data::dl8::phase_3_key_provider(),
)?;
// Reload for the new key providers
self.reload()?;
self.internal_migrate_or_create_batch(
"phase 4 - system entries",
migration_data::dl8::phase_4_system_entries(),
)?;
// Reload for the new system entries
self.reload()?;
// Domain info is now ready and reloaded, we can proceed.
self.set_phase(ServerPhase::DomainInfoReady);
// Bring up the IDM entries.
self.internal_migrate_or_create_batch(
"phase 5 - builtin admin entries",
migration_data::dl8::phase_5_builtin_admin_entries()?,
)?;
self.internal_migrate_or_create_batch(
"phase 6 - builtin not admin entries",
migration_data::dl8::phase_6_builtin_non_admin_entries()?,
)?;
self.internal_migrate_or_create_batch(
"phase 7 - builtin access control profiles",
migration_data::dl8::phase_7_builtin_access_control_profiles(),
)?;
// Reload for all new access controls.
self.reload()?;
Ok(())
}
/// Migration domain level 8 to 9 (1.5.0)
#[instrument(level = "info", skip_all)]
pub(crate) fn migrate_domain_8_to_9(&mut self) -> Result<(), OperationError> {
@ -303,39 +380,61 @@ impl QueryServerWriteTransaction<'_> {
}
// =========== Apply changes ==============
self.internal_migrate_or_create_batch(
"phase 1 - schema attrs",
migration_data::dl9::phase_1_schema_attrs(),
)?;
// Now update schema
let idm_schema_changes = [
SCHEMA_ATTR_OAUTH2_DEVICE_FLOW_ENABLE_DL9.clone().into(),
SCHEMA_ATTR_DOMAIN_ALLOW_EASTER_EGGS_DL9.clone().into(),
SCHEMA_CLASS_OAUTH2_RS_DL9.clone().into(),
SCHEMA_CLASS_DOMAIN_INFO_DL9.clone().into(),
];
idm_schema_changes
.into_iter()
.try_for_each(|entry| self.internal_migrate_or_create(entry))
.map_err(|err| {
error!(?err, "migrate_domain_8_to_9 -> Error");
err
})?;
self.internal_migrate_or_create_batch(
"phase 2 - schema classes",
migration_data::dl9::phase_2_schema_classes(),
)?;
// Reload for the new schema.
self.reload()?;
let idm_data = [
IDM_ACP_OAUTH2_MANAGE_DL9.clone().into(),
IDM_ACP_GROUP_MANAGE_DL9.clone().into(),
IDM_ACP_DOMAIN_ADMIN_DL9.clone().into(),
];
// Reindex?
self.reindex(false)?;
idm_data
.into_iter()
.try_for_each(|entry| self.internal_migrate_or_create(entry))
.map_err(|err| {
error!(?err, "migrate_domain_8_to_9 -> Error");
err
})?;
// Set Phase
self.set_phase(ServerPhase::SchemaReady);
self.internal_migrate_or_create_batch(
"phase 3 - key provider",
migration_data::dl9::phase_3_key_provider(),
)?;
// Reload for the new key providers
self.reload()?;
self.internal_migrate_or_create_batch(
"phase 4 - system entries",
migration_data::dl9::phase_4_system_entries(),
)?;
// Reload for the new system entries
self.reload()?;
// Domain info is now ready and reloaded, we can proceed.
self.set_phase(ServerPhase::DomainInfoReady);
// Bring up the IDM entries.
self.internal_migrate_or_create_batch(
"phase 5 - builtin admin entries",
migration_data::dl9::phase_5_builtin_admin_entries()?,
)?;
self.internal_migrate_or_create_batch(
"phase 6 - builtin not admin entries",
migration_data::dl9::phase_6_builtin_non_admin_entries()?,
)?;
self.internal_migrate_or_create_batch(
"phase 7 - builtin access control profiles",
migration_data::dl9::phase_7_builtin_access_control_profiles(),
)?;
// Reload for all new access controls.
self.reload()?;
Ok(())
@ -350,58 +449,7 @@ impl QueryServerWriteTransaction<'_> {
debug_assert!(*self.phase >= ServerPhase::SchemaReady);
let idm_data = [
IDM_ACP_ACCOUNT_MAIL_READ_DL6.clone().into(),
IDM_ACP_ACCOUNT_SELF_WRITE_V1.clone().into(),
IDM_ACP_ACCOUNT_UNIX_EXTEND_V1.clone().into(),
IDM_ACP_ACP_MANAGE_V1.clone().into(),
IDM_ACP_ALL_ACCOUNTS_POSIX_READ_V1.clone().into(),
IDM_ACP_APPLICATION_ENTRY_MANAGER_DL8.clone().into(),
IDM_ACP_APPLICATION_MANAGE_DL8.clone().into(),
IDM_ACP_DOMAIN_ADMIN_DL8.clone().into(),
IDM_ACP_GROUP_ACCOUNT_POLICY_MANAGE_DL8.clone().into(),
IDM_ACP_GROUP_ENTRY_MANAGED_BY_MODIFY_V1.clone().into(),
IDM_ACP_GROUP_ENTRY_MANAGER_V1.clone().into(),
IDM_ACP_GROUP_MANAGE_DL6.clone().into(),
IDM_ACP_GROUP_READ_V1.clone().into(),
IDM_ACP_GROUP_UNIX_MANAGE_V1.clone().into(),
IDM_ACP_HP_CLIENT_CERTIFICATE_MANAGER_DL7.clone().into(),
IDM_ACP_HP_GROUP_UNIX_MANAGE_V1.clone().into(),
IDM_ACP_HP_PEOPLE_CREDENTIAL_RESET_V1.clone().into(),
IDM_ACP_HP_SERVICE_ACCOUNT_ENTRY_MANAGED_BY_MODIFY_V1
.clone()
.into(),
IDM_ACP_MAIL_SERVERS_DL8.clone().into(),
IDM_ACP_OAUTH2_MANAGE_DL7.clone().into(),
IDM_ACP_PEOPLE_CREATE_DL6.clone().into(),
IDM_ACP_PEOPLE_CREDENTIAL_RESET_V1.clone().into(),
IDM_ACP_PEOPLE_DELETE_V1.clone().into(),
IDM_ACP_PEOPLE_MANAGE_V1.clone().into(),
IDM_ACP_PEOPLE_PII_MANAGE_V1.clone().into(),
IDM_ACP_PEOPLE_PII_READ_V1.clone().into(),
IDM_ACP_PEOPLE_READ_V1.clone().into(),
IDM_ACP_PEOPLE_SELF_WRITE_MAIL_V1.clone().into(),
IDM_ACP_RADIUS_SECRET_MANAGE_V1.clone().into(),
IDM_ACP_RADIUS_SERVERS_V1.clone().into(),
IDM_ACP_RECYCLE_BIN_REVIVE_V1.clone().into(),
IDM_ACP_RECYCLE_BIN_SEARCH_V1.clone().into(),
IDM_ACP_SCHEMA_WRITE_ATTRS_V1.clone().into(),
IDM_ACP_SCHEMA_WRITE_CLASSES_V1.clone().into(),
IDM_ACP_SELF_NAME_WRITE_DL7.clone().into(),
IDM_ACP_SELF_READ_DL8.clone().into(),
IDM_ACP_SELF_WRITE_DL8.clone().into(),
IDM_ACP_SERVICE_ACCOUNT_CREATE_V1.clone().into(),
IDM_ACP_SERVICE_ACCOUNT_DELETE_V1.clone().into(),
IDM_ACP_SERVICE_ACCOUNT_ENTRY_MANAGED_BY_MODIFY_V1
.clone()
.into(),
IDM_ACP_SERVICE_ACCOUNT_ENTRY_MANAGER_V1.clone().into(),
IDM_ACP_SERVICE_ACCOUNT_MANAGE_V1.clone().into(),
IDM_ACP_SYNC_ACCOUNT_MANAGE_V1.clone().into(),
IDM_ACP_SYSTEM_CONFIG_ACCOUNT_POLICY_MANAGE_V1
.clone()
.into(),
];
let idm_data = migration_data::dl9::phase_7_builtin_access_control_profiles();
idm_data
.into_iter()
@ -425,17 +473,62 @@ impl QueryServerWriteTransaction<'_> {
}
// =========== Apply changes ==============
self.internal_migrate_or_create_batch(
"phase 1 - schema attrs",
migration_data::dl10::phase_1_schema_attrs(),
)?;
// Now update schema
let idm_schema_changes = [SCHEMA_CLASS_DOMAIN_INFO_DL10.clone().into()];
self.internal_migrate_or_create_batch(
"phase 2 - schema classes",
migration_data::dl10::phase_2_schema_classes(),
)?;
idm_schema_changes
.into_iter()
.try_for_each(|entry| self.internal_migrate_or_create(entry))
.map_err(|err| {
error!(?err, "migrate_domain_9_to_10 -> Error");
err
})?;
// Reload for the new schema.
self.reload()?;
// Since we just loaded in a ton of schema, lets reindex it incase we added
// new indexes, or this is a bootstrap and we have no indexes yet.
self.reindex(false)?;
// Set Phase
// Indicate the schema is now ready, which allows dyngroups to work when they
// are created in the next phase of migrations.
self.set_phase(ServerPhase::SchemaReady);
self.internal_migrate_or_create_batch(
"phase 3 - key provider",
migration_data::dl10::phase_3_key_provider(),
)?;
// Reload for the new key providers
self.reload()?;
self.internal_migrate_or_create_batch(
"phase 4 - system entries",
migration_data::dl10::phase_4_system_entries(),
)?;
// Reload for the new system entries
self.reload()?;
// Domain info is now ready and reloaded, we can proceed.
self.set_phase(ServerPhase::DomainInfoReady);
// Bring up the IDM entries.
self.internal_migrate_or_create_batch(
"phase 5 - builtin admin entries",
migration_data::dl10::phase_5_builtin_admin_entries()?,
)?;
self.internal_migrate_or_create_batch(
"phase 6 - builtin not admin entries",
migration_data::dl10::phase_6_builtin_non_admin_entries()?,
)?;
self.internal_migrate_or_create_batch(
"phase 7 - builtin access control profiles",
migration_data::dl10::phase_7_builtin_access_control_profiles(),
)?;
self.reload()?;
@ -454,7 +547,7 @@ impl QueryServerWriteTransaction<'_> {
}
#[instrument(level = "info", skip_all)]
pub fn initialise_schema_core(&mut self) -> Result<(), OperationError> {
pub(crate) fn initialise_schema_core(&mut self) -> Result<(), OperationError> {
admin_debug!("initialise_schema_core -> start ...");
// Load in all the "core" schema, that we already have in "memory".
let entries = self.schema.to_entries();
@ -475,320 +568,6 @@ impl QueryServerWriteTransaction<'_> {
debug_assert!(r.is_ok());
r
}
#[instrument(level = "info", skip_all)]
pub fn initialise_schema_idm(&mut self) -> Result<(), OperationError> {
admin_debug!("initialise_schema_idm -> start ...");
// ⚠️ DOMAIN LEVEL 1 SCHEMA ATTRIBUTES ⚠️
// Future schema attributes need to be added via migrations.
//
// DO NOT MODIFY THIS DEFINITION
let idm_schema_attrs = [
SCHEMA_ATTR_SYNC_CREDENTIAL_PORTAL.clone().into(),
SCHEMA_ATTR_SYNC_YIELD_AUTHORITY.clone().into(),
];
let r: Result<(), _> = idm_schema_attrs
.into_iter()
.try_for_each(|entry| self.internal_migrate_or_create(entry));
if r.is_err() {
error!(res = ?r, "initialise_schema_idm -> Error");
}
debug_assert!(r.is_ok());
// ⚠️ DOMAIN LEVEL 1 SCHEMA ATTRIBUTES ⚠️
// Future schema classes need to be added via migrations.
//
// DO NOT MODIFY THIS DEFINITION
let idm_schema: Vec<EntryInitNew> = vec![
// SCHEMA_ATTR_MAIL.clone().into(),
SCHEMA_ATTR_ACCOUNT_EXPIRE.clone().into(),
SCHEMA_ATTR_ACCOUNT_VALID_FROM.clone().into(),
SCHEMA_ATTR_API_TOKEN_SESSION.clone().into(),
SCHEMA_ATTR_AUTH_SESSION_EXPIRY.clone().into(),
SCHEMA_ATTR_AUTH_PRIVILEGE_EXPIRY.clone().into(),
SCHEMA_ATTR_AUTH_PASSWORD_MINIMUM_LENGTH.clone().into(),
SCHEMA_ATTR_BADLIST_PASSWORD.clone().into(),
SCHEMA_ATTR_CREDENTIAL_UPDATE_INTENT_TOKEN.clone().into(),
SCHEMA_ATTR_ATTESTED_PASSKEYS.clone().into(),
// SCHEMA_ATTR_DISPLAYNAME.clone().into(),
SCHEMA_ATTR_DOMAIN_DISPLAY_NAME.clone().into(),
SCHEMA_ATTR_DOMAIN_LDAP_BASEDN.clone().into(),
SCHEMA_ATTR_DOMAIN_NAME.clone().into(),
SCHEMA_ATTR_LDAP_ALLOW_UNIX_PW_BIND.clone().into(),
SCHEMA_ATTR_DOMAIN_SSID.clone().into(),
SCHEMA_ATTR_DOMAIN_TOKEN_KEY.clone().into(),
SCHEMA_ATTR_DOMAIN_UUID.clone().into(),
SCHEMA_ATTR_DYNGROUP_FILTER.clone().into(),
SCHEMA_ATTR_EC_KEY_PRIVATE.clone().into(),
SCHEMA_ATTR_ES256_PRIVATE_KEY_DER.clone().into(),
SCHEMA_ATTR_FERNET_PRIVATE_KEY_STR.clone().into(),
SCHEMA_ATTR_GIDNUMBER.clone().into(),
SCHEMA_ATTR_GRANT_UI_HINT.clone().into(),
SCHEMA_ATTR_JWS_ES256_PRIVATE_KEY.clone().into(),
// SCHEMA_ATTR_LEGALNAME.clone().into(),
SCHEMA_ATTR_LOGINSHELL.clone().into(),
SCHEMA_ATTR_NAME_HISTORY.clone().into(),
SCHEMA_ATTR_NSUNIQUEID.clone().into(),
SCHEMA_ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE
.clone()
.into(),
SCHEMA_ATTR_OAUTH2_CONSENT_SCOPE_MAP.clone().into(),
SCHEMA_ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE.clone().into(),
SCHEMA_ATTR_OAUTH2_PREFER_SHORT_USERNAME.clone().into(),
SCHEMA_ATTR_OAUTH2_RS_BASIC_SECRET.clone().into(),
SCHEMA_ATTR_OAUTH2_RS_IMPLICIT_SCOPES.clone().into(),
SCHEMA_ATTR_OAUTH2_RS_NAME.clone().into(),
SCHEMA_ATTR_OAUTH2_RS_ORIGIN_LANDING.clone().into(),
// SCHEMA_ATTR_OAUTH2_RS_ORIGIN.clone().into(),
SCHEMA_ATTR_OAUTH2_RS_SCOPE_MAP.clone().into(),
SCHEMA_ATTR_OAUTH2_RS_SUP_SCOPE_MAP.clone().into(),
SCHEMA_ATTR_OAUTH2_RS_TOKEN_KEY.clone().into(),
SCHEMA_ATTR_OAUTH2_SESSION.clone().into(),
SCHEMA_ATTR_PASSKEYS.clone().into(),
SCHEMA_ATTR_PRIMARY_CREDENTIAL.clone().into(),
SCHEMA_ATTR_PRIVATE_COOKIE_KEY.clone().into(),
SCHEMA_ATTR_RADIUS_SECRET.clone().into(),
SCHEMA_ATTR_RS256_PRIVATE_KEY_DER.clone().into(),
SCHEMA_ATTR_SSH_PUBLICKEY.clone().into(),
SCHEMA_ATTR_SYNC_COOKIE.clone().into(),
SCHEMA_ATTR_SYNC_TOKEN_SESSION.clone().into(),
SCHEMA_ATTR_UNIX_PASSWORD.clone().into(),
SCHEMA_ATTR_USER_AUTH_TOKEN_SESSION.clone().into(),
SCHEMA_ATTR_DENIED_NAME.clone().into(),
SCHEMA_ATTR_CREDENTIAL_TYPE_MINIMUM.clone().into(),
SCHEMA_ATTR_WEBAUTHN_ATTESTATION_CA_LIST.clone().into(),
// DL4
SCHEMA_ATTR_OAUTH2_RS_CLAIM_MAP_DL4.clone().into(),
SCHEMA_ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT_DL4
.clone()
.into(),
// DL5
// DL6
SCHEMA_ATTR_LIMIT_SEARCH_MAX_RESULTS_DL6.clone().into(),
SCHEMA_ATTR_LIMIT_SEARCH_MAX_FILTER_TEST_DL6.clone().into(),
SCHEMA_ATTR_KEY_INTERNAL_DATA_DL6.clone().into(),
SCHEMA_ATTR_KEY_PROVIDER_DL6.clone().into(),
SCHEMA_ATTR_KEY_ACTION_ROTATE_DL6.clone().into(),
SCHEMA_ATTR_KEY_ACTION_REVOKE_DL6.clone().into(),
SCHEMA_ATTR_KEY_ACTION_IMPORT_JWS_ES256_DL6.clone().into(),
// DL7
SCHEMA_ATTR_PATCH_LEVEL_DL7.clone().into(),
SCHEMA_ATTR_DOMAIN_DEVELOPMENT_TAINT_DL7.clone().into(),
SCHEMA_ATTR_REFERS_DL7.clone().into(),
SCHEMA_ATTR_CERTIFICATE_DL7.clone().into(),
SCHEMA_ATTR_OAUTH2_RS_ORIGIN_DL7.clone().into(),
SCHEMA_ATTR_OAUTH2_STRICT_REDIRECT_URI_DL7.clone().into(),
SCHEMA_ATTR_MAIL_DL7.clone().into(),
SCHEMA_ATTR_LEGALNAME_DL7.clone().into(),
SCHEMA_ATTR_DISPLAYNAME_DL7.clone().into(),
// DL8
SCHEMA_ATTR_LINKED_GROUP_DL8.clone().into(),
SCHEMA_ATTR_APPLICATION_PASSWORD_DL8.clone().into(),
SCHEMA_ATTR_ALLOW_PRIMARY_CRED_FALLBACK_DL8.clone().into(),
];
let r = idm_schema
.into_iter()
// Each item individually logs it's result
.try_for_each(|entry| self.internal_migrate_or_create(entry));
if r.is_err() {
error!(res = ?r, "initialise_schema_idm -> Error");
}
debug_assert!(r.is_ok());
// ⚠️ DOMAIN LEVEL 1 SCHEMA CLASSES ⚠️
// Future schema classes need to be added via migrations.
//
// DO NOT MODIFY THIS DEFINITION
let idm_schema_classes_dl1: Vec<EntryInitNew> = vec![
SCHEMA_CLASS_DYNGROUP.clone().into(),
SCHEMA_CLASS_ORGPERSON.clone().into(),
SCHEMA_CLASS_POSIXACCOUNT.clone().into(),
SCHEMA_CLASS_POSIXGROUP.clone().into(),
SCHEMA_CLASS_SYSTEM_CONFIG.clone().into(),
// DL4
SCHEMA_CLASS_OAUTH2_RS_PUBLIC_DL4.clone().into(),
// DL5
// SCHEMA_CLASS_PERSON_DL5.clone().into(),
SCHEMA_CLASS_ACCOUNT_DL5.clone().into(),
// SCHEMA_CLASS_OAUTH2_RS_DL5.clone().into(),
SCHEMA_CLASS_OAUTH2_RS_BASIC_DL5.clone().into(),
// DL6
// SCHEMA_CLASS_ACCOUNT_POLICY_DL6.clone().into(),
// SCHEMA_CLASS_SERVICE_ACCOUNT_DL6.clone().into(),
// SCHEMA_CLASS_SYNC_ACCOUNT_DL6.clone().into(),
SCHEMA_CLASS_GROUP_DL6.clone().into(),
SCHEMA_CLASS_KEY_PROVIDER_DL6.clone().into(),
SCHEMA_CLASS_KEY_PROVIDER_INTERNAL_DL6.clone().into(),
SCHEMA_CLASS_KEY_OBJECT_DL6.clone().into(),
SCHEMA_CLASS_KEY_OBJECT_JWT_ES256_DL6.clone().into(),
SCHEMA_CLASS_KEY_OBJECT_JWE_A128GCM_DL6.clone().into(),
SCHEMA_CLASS_KEY_OBJECT_INTERNAL_DL6.clone().into(),
// SCHEMA_CLASS_DOMAIN_INFO_DL6.clone().into(),
// DL7
// SCHEMA_CLASS_DOMAIN_INFO_DL7.clone().into(),
SCHEMA_CLASS_SERVICE_ACCOUNT_DL7.clone().into(),
SCHEMA_CLASS_SYNC_ACCOUNT_DL7.clone().into(),
SCHEMA_CLASS_CLIENT_CERTIFICATE_DL7.clone().into(),
SCHEMA_CLASS_OAUTH2_RS_DL7.clone().into(),
// DL8
SCHEMA_CLASS_ACCOUNT_POLICY_DL8.clone().into(),
SCHEMA_CLASS_APPLICATION_DL8.clone().into(),
SCHEMA_CLASS_PERSON_DL8.clone().into(),
SCHEMA_CLASS_DOMAIN_INFO_DL8.clone().into(),
];
let r: Result<(), _> = idm_schema_classes_dl1
.into_iter()
.try_for_each(|entry| self.internal_migrate_or_create(entry));
if r.is_err() {
error!(res = ?r, "initialise_schema_idm -> Error");
}
debug_assert!(r.is_ok());
debug!("initialise_schema_idm -> Ok!");
r
}
#[instrument(level = "info", skip_all)]
/// This function is idempotent, runs all the startup functionality and checks
pub fn initialise_domain_info(&mut self) -> Result<(), OperationError> {
// Configure the default key provider. This needs to exist *before* the
// domain info!
self.internal_migrate_or_create(E_KEY_PROVIDER_INTERNAL_DL6.clone())
.and_then(|_| self.reload())
.map_err(|err| {
error!(?err, "initialise_domain_info::E_KEY_PROVIDER_INTERNAL_DL6");
debug_assert!(false);
err
})?;
self.internal_migrate_or_create(E_SYSTEM_INFO_V1.clone())
.and_then(|_| self.internal_migrate_or_create(E_DOMAIN_INFO_DL6.clone()))
.and_then(|_| self.internal_migrate_or_create(E_SYSTEM_CONFIG_V1.clone()))
.map_err(|err| {
error!(?err, "initialise_domain_info");
debug_assert!(false);
err
})
}
#[instrument(level = "info", skip_all)]
/// This function is idempotent, runs all the startup functionality and checks
pub fn initialise_idm(&mut self) -> Result<(), OperationError> {
// The domain info now exists, we should be able to do these migrations as they will
// cause SPN regenerations to occur
// Delete entries that no longer need to exist.
// TODO: Shouldn't this be a migration?
// Check the admin object exists (migrations).
// Create the default idm_admin group.
let admin_entries: Vec<EntryInitNew> = idm_builtin_admin_entries()?;
let res: Result<(), _> = admin_entries
.into_iter()
// Each item individually logs it's result
.try_for_each(|ent| self.internal_migrate_or_create(ent));
if res.is_ok() {
debug!("initialise_idm p1 -> result Ok!");
} else {
error!(?res, "initialise_idm p1 -> result");
}
debug_assert!(res.is_ok());
res?;
let res: Result<(), _> = idm_builtin_non_admin_groups()
.into_iter()
.try_for_each(|e| self.internal_migrate_or_create(e.clone().try_into()?));
if res.is_ok() {
debug!("initialise_idm p2 -> result Ok!");
} else {
error!(?res, "initialise_idm p2 -> result");
}
debug_assert!(res.is_ok());
res?;
// ⚠️ DOMAIN LEVEL 1 ENTRIES ⚠️
// Future entries need to be added via migrations.
//
// DO NOT MODIFY THIS DEFINITION
let idm_entries: Vec<BuiltinAcp> = vec![
// Built in access controls.
IDM_ACP_RECYCLE_BIN_SEARCH_V1.clone(),
IDM_ACP_RECYCLE_BIN_REVIVE_V1.clone(),
IDM_ACP_SCHEMA_WRITE_ATTRS_V1.clone(),
IDM_ACP_SCHEMA_WRITE_CLASSES_V1.clone(),
IDM_ACP_ACP_MANAGE_V1.clone(),
IDM_ACP_GROUP_ENTRY_MANAGED_BY_MODIFY_V1.clone(),
IDM_ACP_GROUP_ENTRY_MANAGER_V1.clone(),
IDM_ACP_SYNC_ACCOUNT_MANAGE_V1.clone(),
IDM_ACP_RADIUS_SERVERS_V1.clone(),
IDM_ACP_RADIUS_SECRET_MANAGE_V1.clone(),
IDM_ACP_PEOPLE_SELF_WRITE_MAIL_V1.clone(),
// IDM_ACP_SELF_READ_V1.clone(),
// IDM_ACP_SELF_WRITE_V1.clone(),
IDM_ACP_ACCOUNT_SELF_WRITE_V1.clone(),
// IDM_ACP_SELF_NAME_WRITE_V1.clone(),
IDM_ACP_ALL_ACCOUNTS_POSIX_READ_V1.clone(),
IDM_ACP_SYSTEM_CONFIG_ACCOUNT_POLICY_MANAGE_V1.clone(),
IDM_ACP_GROUP_UNIX_MANAGE_V1.clone(),
IDM_ACP_HP_GROUP_UNIX_MANAGE_V1.clone(),
IDM_ACP_GROUP_READ_V1.clone(),
IDM_ACP_ACCOUNT_UNIX_EXTEND_V1.clone(),
IDM_ACP_PEOPLE_PII_READ_V1.clone(),
IDM_ACP_PEOPLE_PII_MANAGE_V1.clone(),
IDM_ACP_PEOPLE_READ_V1.clone(),
IDM_ACP_PEOPLE_MANAGE_V1.clone(),
IDM_ACP_PEOPLE_DELETE_V1.clone(),
IDM_ACP_PEOPLE_CREDENTIAL_RESET_V1.clone(),
IDM_ACP_HP_PEOPLE_CREDENTIAL_RESET_V1.clone(),
IDM_ACP_SERVICE_ACCOUNT_CREATE_V1.clone(),
IDM_ACP_SERVICE_ACCOUNT_DELETE_V1.clone(),
IDM_ACP_SERVICE_ACCOUNT_ENTRY_MANAGER_V1.clone(),
IDM_ACP_SERVICE_ACCOUNT_ENTRY_MANAGED_BY_MODIFY_V1.clone(),
IDM_ACP_HP_SERVICE_ACCOUNT_ENTRY_MANAGED_BY_MODIFY_V1.clone(),
IDM_ACP_SERVICE_ACCOUNT_MANAGE_V1.clone(),
// DL4
// DL5
// IDM_ACP_OAUTH2_MANAGE_DL5.clone(),
// DL6
// IDM_ACP_GROUP_ACCOUNT_POLICY_MANAGE_DL6.clone(),
IDM_ACP_PEOPLE_CREATE_DL6.clone(),
IDM_ACP_GROUP_MANAGE_DL6.clone(),
IDM_ACP_ACCOUNT_MAIL_READ_DL6.clone(),
// IDM_ACP_DOMAIN_ADMIN_DL6.clone(),
// DL7
// IDM_ACP_SELF_WRITE_DL7.clone(),
IDM_ACP_SELF_NAME_WRITE_DL7.clone(),
IDM_ACP_HP_CLIENT_CERTIFICATE_MANAGER_DL7.clone(),
IDM_ACP_OAUTH2_MANAGE_DL7.clone(),
// DL8
IDM_ACP_SELF_READ_DL8.clone(),
IDM_ACP_SELF_WRITE_DL8.clone(),
IDM_ACP_APPLICATION_MANAGE_DL8.clone(),
IDM_ACP_APPLICATION_ENTRY_MANAGER_DL8.clone(),
IDM_ACP_MAIL_SERVERS_DL8.clone(),
IDM_ACP_DOMAIN_ADMIN_DL8.clone(),
IDM_ACP_GROUP_ACCOUNT_POLICY_MANAGE_DL8.clone(),
];
let res: Result<(), _> = idm_entries
.into_iter()
.try_for_each(|entry| self.internal_migrate_or_create(entry.into()));
if res.is_ok() {
admin_debug!("initialise_idm p3 -> result Ok!");
} else {
admin_error!(?res, "initialise_idm p3 -> result");
}
debug_assert!(res.is_ok());
res
}
}
impl QueryServerReadTransaction<'_> {

View file

@ -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> {
@ -2327,8 +2420,17 @@ impl<'a> QueryServerWriteTransaction<'a> {
debug!(domain_previous_version = ?previous_version, domain_target_version = ?domain_info_version);
debug!(domain_previous_patch_level = ?previous_patch_level, domain_target_patch_level = ?domain_info_patch_level);
// We have to check for DL0 since that's the initialisation level.
if previous_version < DOMAIN_MIN_REMIGRATION_LEVEL && previous_version != DOMAIN_LEVEL_0 {
// We have to check for DL0 since that's the initialisation level. If we are at DL0 then
// the server was just brought up and there are no other actions to take since we are
// now at TGT level.
if previous_version == DOMAIN_LEVEL_0 {
debug!(
"Server was just brought up, skipping migrations as we are already at target level"
);
return Ok(());
}
if previous_version < DOMAIN_MIN_REMIGRATION_LEVEL {
error!("UNABLE TO PROCEED. You are attempting a Skip update which is NOT SUPPORTED. You must upgrade one-version of Kanidm at a time.");
error!("For more see: https://kanidm.github.io/kanidm/stable/support.html#upgrade-policy and https://kanidm.github.io/kanidm/stable/server_updates.html");
error!(domain_previous_version = ?previous_version, domain_target_version = ?domain_info_version);
@ -2341,7 +2443,10 @@ impl<'a> QueryServerWriteTransaction<'a> {
self.migrate_domain_8_to_9()?;
}
if previous_patch_level < PATCH_LEVEL_2 && domain_info_patch_level >= PATCH_LEVEL_2 {
if previous_patch_level < PATCH_LEVEL_2
&& domain_info_patch_level >= PATCH_LEVEL_2
&& domain_info_version == DOMAIN_LEVEL_9
{
self.migrate_domain_patch_level_2()?;
}
@ -2480,7 +2585,10 @@ impl<'a> QueryServerWriteTransaction<'a> {
}
fn set_phase(&mut self, phase: ServerPhase) {
*self.phase = phase
// Phase changes are one way
if phase > *self.phase {
*self.phase = phase
}
}
pub(crate) fn get_phase(&mut self) -> ServerPhase {
@ -2625,7 +2733,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 +3187,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);
}
}

View file

@ -615,9 +615,11 @@ mod tests {
Err(OperationError::EmptyRequest)
);
let idm_admin = server_txn.internal_search_uuid(UUID_IDM_ADMIN).unwrap();
// Mod changes no objects
let me_nochg = ModifyEvent::new_impersonate_entry_ser(
BUILTIN_ACCOUNT_IDM_ADMIN.clone(),
let me_nochg = ModifyEvent::new_impersonate_entry(
idm_admin,
filter!(f_eq(
Attribute::Name,
PartialValue::new_iname("flarbalgarble")

View file

@ -4,7 +4,9 @@
//!
use kanidmd_lib::constants::entries::Attribute;
use kanidmd_lib::constants::groups::{idm_builtin_admin_groups, idm_builtin_non_admin_groups};
use kanidmd_lib::migration_data::current::{
phase_5_builtin_admin_entries, phase_6_builtin_non_admin_entries
};
use kanidmd_lib::prelude::{builtin_accounts, EntryInitNew};
use petgraph::graphmap::{AllEdges, GraphMap, NodeTrait};
use petgraph::Directed;
@ -106,8 +108,8 @@ async fn enumerate_default_groups(/*_client: KanidmClient*/) {
graph.add_node(account.uuid);
});
let mut groups = idm_builtin_admin_groups();
groups.extend(idm_builtin_non_admin_groups());
let mut groups = phase_5_builtin_admin_entries();
groups.extend(phase_6_builtin_non_admin_entries());
groups.into_iter().for_each(|group| {
uuidmap.insert(group.uuid, group.clone().try_into().unwrap());

View file

@ -18,7 +18,7 @@ use kanidm_client::{KanidmClient, KanidmClientBuilder};
use kanidm_proto::internal::{Filter, Modify, ModifyList};
use kanidmd_core::config::{Configuration, IntegrationTestConfig};
use kanidmd_core::{create_server_core, CoreHandle};
use kanidmd_lib::prelude::{Attribute, BUILTIN_GROUP_IDM_ADMINS_V1};
use kanidmd_lib::prelude::Attribute;
use tokio::task;
pub const ADMIN_TEST_USER: &str = "admin";
@ -385,7 +385,7 @@ pub async fn login_put_admin_idm_admins(rsclient: &KanidmClient) {
#[allow(clippy::expect_used)]
rsclient
.idm_group_add_members(BUILTIN_GROUP_IDM_ADMINS_V1.name, &[ADMIN_TEST_USER])
.idm_group_add_members("system_admins", &[ADMIN_TEST_USER])
.await
.expect("Failed to add admin user to idm_admins")
}

View file

@ -26,6 +26,19 @@ async fn test_idm_domain_set_ldap_basedn(rsclient: KanidmClient) {
.expect("Failed to set idm_domain_set_ldap_basedn");
}
#[kanidmd_testkit::test]
async fn test_idm_domain_set_ldap_max_queryable_attrs(rsclient: KanidmClient) {
rsclient
.auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
.await
.expect("Failed to login as admin");
rsclient
.idm_domain_set_ldap_max_queryable_attrs(30)
.await
.expect("Failed to set idm_domain_set_ldap_max_queryable_attrs");
}
#[kanidmd_testkit::test]
async fn test_idm_domain_set_display_name(rsclient: KanidmClient) {
rsclient

View file

@ -231,6 +231,19 @@ async fn test_idm_domain_set_ldap_basedn(rsclient: KanidmClient) {
.is_err());
}
#[kanidmd_testkit::test]
async fn test_idm_domain_set_ldap_max_queryable_attrs(rsclient: KanidmClient) {
login_put_admin_idm_admins(&rsclient).await;
assert!(rsclient
.idm_domain_set_ldap_max_queryable_attrs(20)
.await
.is_ok());
assert!(rsclient
.idm_domain_set_ldap_max_queryable_attrs(10)
.await
.is_ok()); // Ideally this should be "is_err"
}
#[kanidmd_testkit::test]
/// Checks that a built-in group idm_all_persons has the "builtin" class as expected.
async fn test_all_persons_has_builtin_class(rsclient: KanidmClient) {

View file

@ -12,10 +12,9 @@ use kanidm_proto::oauth2::{
AccessTokenRequest, AccessTokenResponse, AuthorisationResponse, GrantTypeReq,
};
use kanidmd_lib::constants::NAME_IDM_ALL_ACCOUNTS;
use kanidmd_lib::prelude::uri::{OAUTH2_AUTHORISE_DEVICE, OAUTH2_TOKEN_ENDPOINT};
use kanidmd_lib::prelude::{
Attribute, IDM_ALL_ACCOUNTS, OAUTH2_SCOPE_EMAIL, OAUTH2_SCOPE_OPENID, OAUTH2_SCOPE_READ,
};
use kanidmd_lib::prelude::{Attribute, OAUTH2_SCOPE_EMAIL, OAUTH2_SCOPE_OPENID, OAUTH2_SCOPE_READ};
use kanidmd_testkit::{
assert_no_cache, ADMIN_TEST_PASSWORD, ADMIN_TEST_USER, IDM_ADMIN_TEST_PASSWORD,
IDM_ADMIN_TEST_USER, NOT_ADMIN_TEST_EMAIL, NOT_ADMIN_TEST_PASSWORD, NOT_ADMIN_TEST_USERNAME,
@ -159,7 +158,7 @@ async fn oauth2_device_flow(rsclient: KanidmClient) {
rsclient
.idm_oauth2_rs_update_scope_map(
TEST_INTEGRATION_RS_ID,
IDM_ALL_ACCOUNTS.name,
NAME_IDM_ALL_ACCOUNTS,
vec![OAUTH2_SCOPE_READ, OAUTH2_SCOPE_EMAIL, OAUTH2_SCOPE_OPENID],
)
.await
@ -168,7 +167,7 @@ async fn oauth2_device_flow(rsclient: KanidmClient) {
rsclient
.idm_oauth2_rs_update_sup_scope_map(
TEST_INTEGRATION_RS_ID,
IDM_ALL_ACCOUNTS.name,
NAME_IDM_ALL_ACCOUNTS,
vec![ADMIN_TEST_USER],
)
.await
@ -179,7 +178,7 @@ async fn oauth2_device_flow(rsclient: KanidmClient) {
.idm_oauth2_rs_update_claim_map(
TEST_INTEGRATION_RS_ID,
"test_claim",
IDM_ALL_ACCOUNTS.name,
NAME_IDM_ALL_ACCOUNTS,
&["claim_a".to_string(), "claim_b".to_string()],
)
.await

View file

@ -12,7 +12,8 @@ use kanidm_proto::oauth2::{
AccessTokenResponse, AccessTokenType, AuthorisationResponse, GrantTypeReq,
OidcDiscoveryResponse,
};
use kanidmd_lib::prelude::{Attribute, IDM_ALL_ACCOUNTS};
use kanidmd_lib::constants::NAME_IDM_ALL_ACCOUNTS;
use kanidmd_lib::prelude::Attribute;
use oauth2_ext::PkceCodeChallenge;
use reqwest::header::{HeaderValue, CONTENT_TYPE};
use reqwest::StatusCode;
@ -98,7 +99,7 @@ async fn test_oauth2_openid_basic_flow_impl(
rsclient
.idm_oauth2_rs_update_scope_map(
TEST_INTEGRATION_RS_ID,
IDM_ALL_ACCOUNTS.name,
NAME_IDM_ALL_ACCOUNTS,
vec![OAUTH2_SCOPE_READ, OAUTH2_SCOPE_EMAIL, OAUTH2_SCOPE_OPENID],
)
.await
@ -107,7 +108,7 @@ async fn test_oauth2_openid_basic_flow_impl(
rsclient
.idm_oauth2_rs_update_sup_scope_map(
TEST_INTEGRATION_RS_ID,
IDM_ALL_ACCOUNTS.name,
NAME_IDM_ALL_ACCOUNTS,
vec![ADMIN_TEST_USER],
)
.await
@ -627,7 +628,7 @@ async fn test_oauth2_openid_public_flow_impl(
rsclient
.idm_oauth2_rs_update_scope_map(
TEST_INTEGRATION_RS_ID,
IDM_ALL_ACCOUNTS.name,
NAME_IDM_ALL_ACCOUNTS,
vec![OAUTH2_SCOPE_READ, OAUTH2_SCOPE_EMAIL, OAUTH2_SCOPE_OPENID],
)
.await
@ -636,7 +637,7 @@ async fn test_oauth2_openid_public_flow_impl(
rsclient
.idm_oauth2_rs_update_sup_scope_map(
TEST_INTEGRATION_RS_ID,
IDM_ALL_ACCOUNTS.name,
NAME_IDM_ALL_ACCOUNTS,
vec![ADMIN_TEST_USER],
)
.await
@ -647,7 +648,7 @@ async fn test_oauth2_openid_public_flow_impl(
.idm_oauth2_rs_update_claim_map(
TEST_INTEGRATION_RS_ID,
"test_claim",
IDM_ALL_ACCOUNTS.name,
NAME_IDM_ALL_ACCOUNTS,
&["claim_a".to_string(), "claim_b".to_string()],
)
.await

View file

@ -3,6 +3,7 @@ use std::path::Path;
use std::time::SystemTime;
use kanidm_proto::constants::{ATTR_GIDNUMBER, KSESSIONID};
use kanidm_proto::internal::{
ApiToken, CURegState, Filter, ImageValue, Modify, ModifyList, UatPurpose, UserAuthToken,
};
@ -10,10 +11,10 @@ use kanidm_proto::v1::{
AuthCredential, AuthIssueSession, AuthMech, AuthRequest, AuthResponse, AuthState, AuthStep,
Entry,
};
use kanidmd_lib::constants::{NAME_IDM_ADMINS, NAME_SYSTEM_ADMINS};
use kanidmd_lib::credential::totp::Totp;
use kanidmd_lib::prelude::{
Attribute, BUILTIN_GROUP_IDM_ADMINS_V1, BUILTIN_GROUP_SYSTEM_ADMINS_V1,
};
use kanidmd_lib::prelude::Attribute;
use tracing::{debug, trace};
use std::str::FromStr;
@ -144,10 +145,7 @@ async fn test_server_rest_group_read(rsclient: KanidmClient) {
let g_list = rsclient.idm_group_list().await.unwrap();
assert!(!g_list.is_empty());
let g = rsclient
.idm_group_get(BUILTIN_GROUP_IDM_ADMINS_V1.name)
.await
.unwrap();
let g = rsclient.idm_group_get(NAME_IDM_ADMINS).await.unwrap();
assert!(g.is_some());
println!("{:?}", g);
}
@ -165,7 +163,7 @@ async fn test_server_rest_group_lifecycle(rsclient: KanidmClient) {
// Create a new group
rsclient
.idm_group_create("demo_group", Some(BUILTIN_GROUP_IDM_ADMINS_V1.name))
.idm_group_create("demo_group", Some(NAME_IDM_ADMINS))
.await
.unwrap();
@ -245,16 +243,13 @@ async fn test_server_rest_group_lifecycle(rsclient: KanidmClient) {
assert_eq!(g_list_3.len(), g_list.len());
// Check we can get an exact group
let g = rsclient
.idm_group_get(BUILTIN_GROUP_IDM_ADMINS_V1.name)
.await
.unwrap();
let g = rsclient.idm_group_get(NAME_IDM_ADMINS).await.unwrap();
assert!(g.is_some());
println!("{:?}", g);
// They should have members
let members = rsclient
.idm_group_get_members(BUILTIN_GROUP_IDM_ADMINS_V1.name)
.idm_group_get_members(NAME_IDM_ADMINS)
.await
.unwrap();
println!("{:?}", members);
@ -326,7 +321,7 @@ async fn test_server_radius_credential_lifecycle(rsclient: KanidmClient) {
// All admin to create persons.
rsclient
.idm_group_add_members(BUILTIN_GROUP_IDM_ADMINS_V1.name, &["admin"])
.idm_group_add_members(NAME_IDM_ADMINS, &["admin"])
.await
.unwrap();
@ -397,7 +392,7 @@ async fn test_server_rest_person_account_lifecycle(rsclient: KanidmClient) {
// To enable the admin to actually make some of these changes, we have
// to make them a people admin. NOT recommended in production!
rsclient
.idm_group_add_members(BUILTIN_GROUP_IDM_ADMINS_V1.name, &["admin"])
.idm_group_add_members(NAME_IDM_ADMINS, &["admin"])
.await
.unwrap();
@ -551,7 +546,7 @@ async fn test_server_rest_posix_lifecycle(rsclient: KanidmClient) {
assert!(res.is_ok());
// Not recommended in production!
rsclient
.idm_group_add_members(BUILTIN_GROUP_IDM_ADMINS_V1.name, &["admin"])
.idm_group_add_members(NAME_IDM_ADMINS, &["admin"])
.await
.unwrap();
@ -571,7 +566,7 @@ async fn test_server_rest_posix_lifecycle(rsclient: KanidmClient) {
// Extend the group with posix attrs
rsclient
.idm_group_create("posix_group", Some(BUILTIN_GROUP_IDM_ADMINS_V1.name))
.idm_group_create("posix_group", Some(NAME_IDM_ADMINS))
.await
.unwrap();
rsclient
@ -676,7 +671,7 @@ async fn test_server_rest_posix_auth_lifecycle(rsclient: KanidmClient) {
// Not recommended in production!
rsclient
.idm_group_add_members(BUILTIN_GROUP_IDM_ADMINS_V1.name, &["admin"])
.idm_group_add_members(NAME_IDM_ADMINS, &["admin"])
.await
.unwrap();
@ -773,7 +768,7 @@ async fn test_server_rest_recycle_lifecycle(rsclient: KanidmClient) {
// Not recommended in production!
rsclient
.idm_group_add_members(BUILTIN_GROUP_IDM_ADMINS_V1.name, &["admin"])
.idm_group_add_members(NAME_IDM_ADMINS, &["admin"])
.await
.unwrap();
@ -826,7 +821,7 @@ async fn test_server_rest_oauth2_basic_lifecycle(rsclient: KanidmClient) {
assert!(res.is_ok());
rsclient
.idm_group_add_members(BUILTIN_GROUP_IDM_ADMINS_V1.name, &["admin"])
.idm_group_add_members(NAME_IDM_ADMINS, &["admin"])
.await
.unwrap();
@ -902,11 +897,7 @@ async fn test_server_rest_oauth2_basic_lifecycle(rsclient: KanidmClient) {
// Check that we can add scope maps and delete them.
rsclient
.idm_oauth2_rs_update_scope_map(
"test_integration",
BUILTIN_GROUP_SYSTEM_ADMINS_V1.name,
vec!["a", "b"],
)
.idm_oauth2_rs_update_scope_map("test_integration", NAME_SYSTEM_ADMINS, vec!["a", "b"])
.await
.expect("Failed to create scope map");
@ -921,11 +912,7 @@ async fn test_server_rest_oauth2_basic_lifecycle(rsclient: KanidmClient) {
// Check we can update a scope map
rsclient
.idm_oauth2_rs_update_scope_map(
"test_integration",
BUILTIN_GROUP_SYSTEM_ADMINS_V1.name,
vec!["a", "b", "c"],
)
.idm_oauth2_rs_update_scope_map("test_integration", NAME_SYSTEM_ADMINS, vec!["a", "b", "c"])
.await
.expect("Failed to create scope map");
@ -1009,7 +996,7 @@ async fn test_server_rest_oauth2_basic_lifecycle(rsclient: KanidmClient) {
// Check we can delete a scope map.
rsclient
.idm_oauth2_rs_delete_scope_map("test_integration", BUILTIN_GROUP_SYSTEM_ADMINS_V1.name)
.idm_oauth2_rs_delete_scope_map("test_integration", NAME_SYSTEM_ADMINS)
.await
.expect("Failed to delete scope map");
@ -1049,7 +1036,7 @@ async fn test_server_credential_update_session_pw(rsclient: KanidmClient) {
// Not recommended in production!
rsclient
.idm_group_add_members(BUILTIN_GROUP_IDM_ADMINS_V1.name, &["admin"])
.idm_group_add_members(NAME_IDM_ADMINS, &["admin"])
.await
.unwrap();
@ -1124,7 +1111,7 @@ async fn test_server_credential_update_session_totp_pw(rsclient: KanidmClient) {
// Not recommended in production!
rsclient
.idm_group_add_members(BUILTIN_GROUP_IDM_ADMINS_V1.name, &["admin"])
.idm_group_add_members(NAME_IDM_ADMINS, &["admin"])
.await
.unwrap();
@ -1253,7 +1240,7 @@ async fn setup_demo_account_passkey(rsclient: &KanidmClient) -> WebauthnAuthenti
// Not recommended in production!
rsclient
.idm_group_add_members(BUILTIN_GROUP_IDM_ADMINS_V1.name, &["admin"])
.idm_group_add_members(NAME_IDM_ADMINS, &["admin"])
.await
.unwrap();
@ -1409,7 +1396,7 @@ async fn test_server_api_token_lifecycle(rsclient: KanidmClient) {
.idm_service_account_create(
test_service_account_username,
"Test Service",
BUILTIN_GROUP_IDM_ADMINS_V1.name,
NAME_IDM_ADMINS,
)
.await
.expect("Failed to create service account");

View file

@ -2,7 +2,8 @@ use compact_jwt::{traits::JwsVerifiable, JwsCompact, JwsEs256Verifier, JwsVerifi
use kanidm_client::KanidmClient;
use kanidm_proto::internal::ScimSyncToken;
use kanidm_proto::scim_v1::ScimEntryGetQuery;
use kanidmd_lib::prelude::{Attribute, BUILTIN_GROUP_IDM_ADMINS_V1};
use kanidmd_lib::constants::NAME_IDM_ADMINS;
use kanidmd_lib::prelude::Attribute;
use kanidmd_testkit::{ADMIN_TEST_PASSWORD, ADMIN_TEST_USER};
use reqwest::header::HeaderValue;
use std::str::FromStr;
@ -170,7 +171,7 @@ async fn test_scim_sync_entry_get(rsclient: KanidmClient) {
// All admin to create persons.
rsclient
.idm_group_add_members(BUILTIN_GROUP_IDM_ADMINS_V1.name, &["admin"])
.idm_group_add_members(NAME_IDM_ADMINS, &["admin"])
.await
.unwrap();

View file

@ -1,5 +1,5 @@
use kanidm_client::KanidmClient;
use kanidmd_lib::prelude::BUILTIN_GROUP_IDM_ADMINS_V1;
use kanidmd_lib::constants::NAME_IDM_ADMINS;
use kanidmd_testkit::*;
#[kanidmd_testkit::test]
@ -8,12 +8,7 @@ async fn account_id_unix_token(rsclient: KanidmClient) {
create_user(&rsclient, "group_manager", "idm_group_manage_priv").await;
// create test user without creating new groups
create_user(
&rsclient,
NOT_ADMIN_TEST_USERNAME,
BUILTIN_GROUP_IDM_ADMINS_V1.name,
)
.await;
create_user(&rsclient, NOT_ADMIN_TEST_USERNAME, NAME_IDM_ADMINS).await;
login_account(&rsclient, "group_manager").await;
let response = rsclient

View file

@ -91,6 +91,18 @@ impl CommonOpt {
false => client_builder,
};
let client_builder = match self.accept_invalid_certs {
true => {
warn!(
"TLS Certificate Verification disabled!!! This can lead to credential and account compromise!!!"
);
client_builder.danger_accept_invalid_certs(true)
}
false => client_builder,
};
let client_builder = client_builder.set_token_cache_path(self.token_cache_path.clone());
client_builder.build().unwrap_or_else(|e| {
error!("Failed to build client instance -- {:?}", e);
std::process::exit(1);

View file

@ -14,7 +14,8 @@ impl DomainOpt {
| DomainOpt::SetLdapAllowUnixPasswordBind { copt, .. }
| DomainOpt::SetAllowEasterEggs { copt, .. }
| DomainOpt::RevokeKey { copt, .. }
| DomainOpt::Show(copt) => copt.debug,
| DomainOpt::Show(copt)
| DomainOpt::SetLdapMaxQueryableAttrs { copt, .. } => copt.debug,
}
}
@ -34,6 +35,23 @@ impl DomainOpt {
Err(e) => handle_client_error(e, opt.copt.output_mode),
}
}
DomainOpt::SetLdapMaxQueryableAttrs {
copt,
new_max_queryable_attrs,
} => {
eprintln!(
"Attempting to set the maximum number of queryable LDAP attributes to: {:?}",
new_max_queryable_attrs
);
let client = copt.to_client(OpType::Write).await;
match client
.idm_domain_set_ldap_max_queryable_attrs(*new_max_queryable_attrs)
.await
{
Ok(_) => println!("Success"),
Err(e) => handle_client_error(e, copt.output_mode),
}
}
DomainOpt::SetLdapBasedn { copt, new_basedn } => {
eprintln!(
"Attempting to set the domain's ldap basedn to: {:?}",

View file

@ -831,6 +831,13 @@ async fn totp_enroll_prompt(session_token: &CUSessionToken, client: &KanidmClien
let label: String = Input::new()
.with_prompt("TOTP Label")
.validate_with(|input: &String| -> Result<(), &str> {
if input.trim().is_empty() {
Err("Label cannot be empty")
} else {
Ok(())
}
})
.interact_text()
.expect("Failed to interact with interactive session");
@ -919,6 +926,13 @@ async fn totp_enroll_prompt(session_token: &CUSessionToken, client: &KanidmClien
eprintln!("Incorrect TOTP code entered. Please try again.");
continue;
}
Ok(CUStatus {
mfaregstate: CURegState::TotpNameTryAgain(label),
..
}) => {
eprintln!("{label} is either invalid or already taken. Please try again.");
continue;
}
Ok(CUStatus {
mfaregstate: CURegState::TotpInvalidSha1,
..

View file

@ -87,6 +87,13 @@ pub struct CommonOpt {
default_value_t = false
)]
skip_hostname_verification: bool,
/// Don't verify CA
#[clap(
long = "accept-invalid-certs",
env = "KANIDM_ACCEPT_INVALID_CERTS",
default_value_t = false
)]
accept_invalid_certs: bool,
/// Path to a file to cache tokens in, defaults to ~/.cache/kanidm_tokens
#[clap(short, long, env = "KANIDM_TOKEN_CACHE_PATH", hide = true, default_value = None,
value_parser = clap::builder::NonEmptyStringValueParser::new())]
@ -198,7 +205,6 @@ pub enum GroupAccountPolicyOpt {
copt: CommonOpt,
},
/// Set the maximum time for privilege session expiry in seconds.
#[clap(name = "privilege-expiry")]
PrivilegedSessionExpiry {
@ -208,7 +214,6 @@ pub enum GroupAccountPolicyOpt {
copt: CommonOpt,
},
/// The WebAuthn attestation CA list that should be enforced
/// on members of this group. Prevents use of passkeys that are
/// not in this list. To create this list, use `fido-mds-tool`
@ -294,7 +299,6 @@ pub enum GroupAccountPolicyOpt {
#[clap(flatten)]
copt: CommonOpt,
},
}
#[derive(Debug, Subcommand)]
@ -1313,6 +1317,14 @@ pub enum DomainOpt {
#[clap[name = "set-displayname"]]
/// Set the domain display name
SetDisplayname(OptSetDomainDisplayname),
/// Sets the maximum number of LDAP attributes that can be queried in one operation.
#[clap[name = "set-ldap-queryable-attrs"]]
SetLdapMaxQueryableAttrs {
#[clap(flatten)]
copt: CommonOpt,
#[clap(name = "maximum-queryable-attrs")]
new_max_queryable_attrs: usize,
},
#[clap[name = "set-ldap-basedn"]]
/// Change the basedn of this server. Takes effect after a server restart.
/// Examples are `o=organisation` or `dc=domain,dc=name`. Must be a valid ldap