20220817 ldap service tokens ()

This commit is contained in:
Firstyear 2022-09-02 14:21:20 +10:00 committed by GitHub
parent e6d4cd2d84
commit 925c03b3fb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
81 changed files with 2834 additions and 3139 deletions

169
Cargo.lock generated
View file

@ -137,7 +137,7 @@ dependencies = [
"num-traits",
"rusticata-macros",
"thiserror",
"time 0.3.13",
"time 0.3.14",
]
[[package]]
@ -213,9 +213,9 @@ dependencies = [
[[package]]
name = "async-global-executor"
version = "2.2.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5262ed948da60dd8956c6c5aca4d4163593dddb7b32d73267c93dab7b2e98940"
checksum = "0da5b41ee986eed3f524c380e6d64965aea573882a8907682ad100f7859305ca"
dependencies = [
"async-channel",
"async-executor",
@ -223,7 +223,6 @@ dependencies = [
"async-lock",
"blocking",
"futures-lite",
"num_cpus",
"once_cell",
"tokio",
]
@ -246,10 +245,11 @@ dependencies = [
[[package]]
name = "async-io"
version = "1.7.0"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5e18f61464ae81cde0a23e713ae8fd299580c54d697a35820cfd0625b8b0e07"
checksum = "0ab006897723d9352f63e2b13047177c3982d8d79709d713ce7747a8f19fd1b0"
dependencies = [
"autocfg",
"concurrent-queue",
"futures-lite",
"libc",
@ -274,11 +274,12 @@ dependencies = [
[[package]]
name = "async-process"
version = "1.4.0"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf2c06e30a24e8c78a3987d07f0930edf76ef35e027e7bdb063fccafdad1f60c"
checksum = "02111fd8655a613c25069ea89fc8d9bb89331fa77486eb3bc059ee757cfa481c"
dependencies = [
"async-io",
"autocfg",
"blocking",
"cfg-if 1.0.0",
"event-listener",
@ -585,9 +586,9 @@ dependencies = [
[[package]]
name = "bumpalo"
version = "3.10.0"
version = "3.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3"
checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d"
[[package]]
name = "byte-tools"
@ -684,9 +685,9 @@ dependencies = [
[[package]]
name = "clap"
version = "3.2.17"
version = "3.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29e724a68d9319343bb3328c9cc2dfde263f4b3142ee1059a9980580171c954b"
checksum = "b15f2ea93df33549dbe2e8eecd1ca55269d63ae0b3ba1f55db030817d1c2867f"
dependencies = [
"atty",
"bitflags",
@ -705,14 +706,14 @@ version = "3.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4179da71abd56c26b54dd0c248cc081c1f43b0a1a7e8448e28e57a29baa993d"
dependencies = [
"clap 3.2.17",
"clap 3.2.18",
]
[[package]]
name = "clap_derive"
version = "3.2.17"
version = "3.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13547f7012c01ab4a0e8f8967730ada8f9fdf419e8b6c792788f39cf4e46eefa"
checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65"
dependencies = [
"heck",
"proc-macro-error",
@ -842,7 +843,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94d4706de1b0fa5b132270cddffa8585166037822e260a944fe161acd137ca05"
dependencies = [
"percent-encoding",
"time 0.3.13",
"time 0.3.14",
"version_check",
]
@ -858,7 +859,7 @@ dependencies = [
"publicsuffix",
"serde",
"serde_json",
"time 0.3.13",
"time 0.3.14",
"url",
]
@ -880,9 +881,9 @@ checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
[[package]]
name = "cpufeatures"
version = "0.2.2"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"
checksum = "dc948ebb96241bb40ab73effeb80d9f93afaad49359d159a5e61be51619fe813"
dependencies = [
"libc",
]
@ -1082,7 +1083,7 @@ dependencies = [
name = "daemon"
version = "1.1.0-alpha.9"
dependencies = [
"clap 3.2.17",
"clap 3.2.18",
"clap_complete",
"kanidm",
"kanidm_proto",
@ -1185,9 +1186,9 @@ dependencies = [
[[package]]
name = "devd-rs"
version = "0.3.4"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9861a4115f5148d32f7bbbadcdc562d5754ed75c8d27316a587440dd2ac4df84"
checksum = "0c315b8fe6f26aea3091b030c28aabbdf491376ae39033978f06e468ab42360c"
dependencies = [
"libc",
"nom 7.1.1",
@ -1277,9 +1278,9 @@ checksum = "4f94fa09c2aeea5b8839e414b7b841bf429fd25b9c522116ac97ee87856d88b2"
[[package]]
name = "either"
version = "1.7.0"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be"
checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
[[package]]
name = "encode_unicode"
@ -1298,9 +1299,9 @@ dependencies = [
[[package]]
name = "erased-serde"
version = "0.3.22"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "003000e712ad0f95857bd4d2ef8d1890069e06554101697d12050668b2f6f020"
checksum = "54558e0ba96fbe24280072642eceb9d7d442e32c7ec0ea9e7ecd7b4ea2cf4e11"
dependencies = [
"serde",
]
@ -1438,9 +1439,9 @@ checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394"
[[package]]
name = "futures"
version = "0.3.23"
version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab30e97ab6aacfe635fad58f22c2bb06c8b685f7421eb1e064a729e2a5f481fa"
checksum = "7f21eda599937fba36daeb58a22e8f5cee2d14c4a17b5b7739c7c8e5e3b8230c"
dependencies = [
"futures-channel",
"futures-core",
@ -1453,9 +1454,9 @@ dependencies = [
[[package]]
name = "futures-channel"
version = "0.3.23"
version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bfc52cbddcfd745bf1740338492bb0bd83d76c67b445f91c5fb29fae29ecaa1"
checksum = "30bdd20c28fadd505d0fd6712cdfcb0d4b5648baf45faef7f852afb2399bb050"
dependencies = [
"futures-core",
"futures-sink",
@ -1463,15 +1464,15 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.23"
version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2acedae88d38235936c3922476b10fced7b2b68136f5e3c03c2d5be348a1115"
checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf"
[[package]]
name = "futures-executor"
version = "0.3.23"
version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d11aa21b5b587a64682c0094c2bdd4df0076c5324961a40cc3abd7f37930528"
checksum = "9ff63c23854bee61b6e9cd331d523909f238fc7636290b96826e9cfa5faa00ab"
dependencies = [
"futures-core",
"futures-task",
@ -1480,9 +1481,9 @@ dependencies = [
[[package]]
name = "futures-io"
version = "0.3.23"
version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93a66fc6d035a26a3ae255a6d2bca35eda63ae4c5512bef54449113f7a1228e5"
checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68"
[[package]]
name = "futures-lite"
@ -1501,9 +1502,9 @@ dependencies = [
[[package]]
name = "futures-macro"
version = "0.3.23"
version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0db9cce532b0eae2ccf2766ab246f114b56b9cf6d445e00c2549fbc100ca045d"
checksum = "42cd15d1c7456c04dbdf7e88bcd69760d74f3a798d6444e16974b505b0e62f17"
dependencies = [
"proc-macro2",
"quote",
@ -1512,21 +1513,21 @@ dependencies = [
[[package]]
name = "futures-sink"
version = "0.3.23"
version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca0bae1fe9752cf7fd9b0064c674ae63f97b37bc714d745cbde0afb7ec4e6765"
checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56"
[[package]]
name = "futures-task"
version = "0.3.23"
version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "842fc63b931f4056a24d59de13fb1272134ce261816e063e634ad0c15cdc5306"
checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1"
[[package]]
name = "futures-util"
version = "0.3.23"
version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0828a5471e340229c11c77ca80017937ce3c58cb788a17e5f1c2d5c485a9577"
checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90"
dependencies = [
"futures-channel",
"futures-core",
@ -1645,10 +1646,11 @@ dependencies = [
[[package]]
name = "gloo-console"
version = "0.2.1"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3907f786f65bbb4f419e918b0c5674175ef1c231ecda93b2dbd65fd1e8882637"
checksum = "82b7ce3c05debe147233596904981848862b068862e9ec3e34be446077190d3f"
dependencies = [
"gloo-utils",
"js-sys",
"serde",
"wasm-bindgen",
@ -1706,9 +1708,9 @@ dependencies = [
[[package]]
name = "gloo-net"
version = "0.2.3"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "351e6f94c76579cc9f9323a15f209086fc7bd428bff4288723d3a417851757b2"
checksum = "ec897194fb9ac576c708f63d35604bc58f2a262b8cec0fabfed26f3991255f21"
dependencies = [
"futures-channel",
"futures-core",
@ -1736,9 +1738,9 @@ dependencies = [
[[package]]
name = "gloo-storage"
version = "0.2.1"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1caa4ba51c99de680dee3ad99c32ca45e9f13311be72079154d222c3f9a6b6f5"
checksum = "5d6ab60bf5dbfd6f0ed1f7843da31b41010515c745735c970e821945ca91e480"
dependencies = [
"gloo-utils",
"js-sys",
@ -1763,20 +1765,22 @@ dependencies = [
[[package]]
name = "gloo-utils"
version = "0.1.4"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "929c53c913bb7a88d75d9dc3e9705f963d8c2b9001510b25ddaf671b9fb7049d"
checksum = "40913a05c8297adca04392f707b1e73b12ba7b8eab7244a4961580b1fd34063c"
dependencies = [
"js-sys",
"serde",
"serde_json",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-worker"
version = "0.2.0"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9caac1b89bbe1e1454bb23e4d046a3fc92438ae2e95fb429c41685789e1fcbaa"
checksum = "13471584da78061a28306d1359dd0178d8d6fc1c7c80e5e35d27260346e0516a"
dependencies = [
"anymap2",
"bincode",
@ -1985,9 +1989,9 @@ dependencies = [
[[package]]
name = "iana-time-zone"
version = "0.1.45"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef5528d9c2817db4e10cc78f8d4c8228906e5854f389ff6b076cee3572a09d35"
checksum = "ad2bfd338099682614d3ee3fe0cd72e0b6a41ca6a87f6a74a3bd593c91650501"
dependencies = [
"android_system_properties",
"core-foundation-sys",
@ -2205,7 +2209,7 @@ dependencies = [
name = "kanidm_tools"
version = "1.1.0-alpha.9"
dependencies = [
"clap 3.2.17",
"clap 3.2.18",
"clap_complete",
"compact_jwt",
"dialoguer",
@ -2233,7 +2237,7 @@ name = "kanidm_unix_int"
version = "1.1.0-alpha.9"
dependencies = [
"bytes",
"clap 3.2.17",
"clap 3.2.18",
"clap_complete",
"futures",
"kanidm",
@ -2436,9 +2440,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "lock_api"
version = "0.4.7"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53"
checksum = "9f80bf5aacaf25cbfc8210d1cfb718f2bf3b11c4c54e5afe36c236853a8ec390"
dependencies = [
"autocfg",
"scopeguard",
@ -2784,7 +2788,7 @@ dependencies = [
name = "orca"
version = "1.1.0-alpha.9"
dependencies = [
"clap 3.2.17",
"clap 3.2.18",
"crossbeam",
"csv",
"dialoguer",
@ -2949,9 +2953,9 @@ checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
[[package]]
name = "plotters"
version = "0.3.2"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9428003b84df1496fb9d6eeee9c5f8145cb41ca375eb0dad204328888832811f"
checksum = "716b4eeb6c4a1d3ecc956f75b43ec2e8e8ba80026413e70a3f41fd3313d3492b"
dependencies = [
"num-traits",
"plotters-backend",
@ -2968,19 +2972,20 @@ checksum = "193228616381fecdc1224c62e96946dfbc73ff4384fba576e052ff8c1bea8142"
[[package]]
name = "plotters-svg"
version = "0.3.2"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0918736323d1baff32ee0eade54984f6f201ad7e97d5cfb5d6ab4a358529615"
checksum = "f9a81d2759aae1dae668f783c308bc5c8ebd191ff4184aaa1b37f65a6ae5a56f"
dependencies = [
"plotters-backend",
]
[[package]]
name = "polling"
version = "2.2.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259"
checksum = "899b00b9c8ab553c743b3e11e87c5c7d423b2a2de229ba95b24a756344748011"
dependencies = [
"autocfg",
"cfg-if 1.0.0",
"libc",
"log",
@ -3500,9 +3505,9 @@ dependencies = [
[[package]]
name = "security-framework"
version = "2.6.1"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc"
checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c"
dependencies = [
"bitflags",
"core-foundation",
@ -3538,9 +3543,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]]
name = "serde"
version = "1.0.143"
version = "1.0.144"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53e8e5d5b70924f74ff5c6d64d9a5acd91422117c60f48c4e07855238a254553"
checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860"
dependencies = [
"serde_derive",
]
@ -3578,9 +3583,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.143"
version = "1.0.144"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3d8e8de557aee63c26b85b947f5e59b690d0454c753f3adeb5cd7835ab88391"
checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00"
dependencies = [
"proc-macro2",
"quote",
@ -3598,9 +3603,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.83"
version = "1.0.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38dd04e3c8279e75b31ef29dbdceebfe5ad89f4d0937213c53f7d49d01b3d5a7"
checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44"
dependencies = [
"itoa 1.0.3",
"ryu",
@ -3789,9 +3794,9 @@ dependencies = [
[[package]]
name = "socket2"
version = "0.4.4"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0"
checksum = "10c98bba371b9b22a71a9414e420f92ddeb2369239af08200816169d5e2dd7aa"
dependencies = [
"libc",
"winapi",
@ -4095,9 +4100,9 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.13"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db76ff9fa4b1458b3c7f077f3ff9887394058460d21e634355b273aaf11eea45"
checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b"
dependencies = [
"itoa 1.0.3",
"libc",
@ -4286,7 +4291,7 @@ dependencies = [
[[package]]
name = "tracing-forest"
version = "0.1.4"
source = "git+https://github.com/Firstyear/tracing-forest.git?rev=18d242a4dde060c4946ade0a2c4d5be1df048aea#18d242a4dde060c4946ade0a2c4d5be1df048aea"
source = "git+https://github.com/QnnOkabayashi/tracing-forest.git?rev=48d78f7294ceee47a22eee5c80964143c4fb3fe1#48d78f7294ceee47a22eee5c80964143c4fb3fe1"
dependencies = [
"smallvec",
"thiserror",
@ -4824,7 +4829,7 @@ dependencies = [
"oid-registry",
"rusticata-macros",
"thiserror",
"time 0.3.13",
"time 0.3.14",
]
[[package]]
@ -4944,5 +4949,5 @@ dependencies = [
"lazy_static",
"quick-error",
"regex",
"time 0.3.13",
"time 0.3.14",
]

View file

@ -13,8 +13,11 @@ if [ -z "$KANI_TMP" ]; then
fi
ALTNAME_FILE="${KANI_TMP}altnames.cnf"
CANAME_FILE="${KANI_TMP}ca.cnf"
CACERT="${KANI_TMP}ca.pem"
CAKEY="${KANI_TMP}cakey.pem"
CADB="${KANI_TMP}ca.txt"
CASRL="${KANI_TMP}ca.srl"
KEYFILE="${KANI_TMP}key.pem"
CERTFILE="${KANI_TMP}cert.pem"
@ -27,7 +30,78 @@ if [ ! -d "${KANI_TMP}" ]; then
mkdir -p "${KANI_TMP}"
fi
cat > "${CANAME_FILE}" << DEVEOF
[req]
nsComment = "Certificate Authority"
distinguished_name = req_distinguished_name
req_extensions = v3_ca
[ req_distinguished_name ]
countryName = Country Name (2 letter code)
countryName_default = AU
countryName_min = 2
countryName_max = 2
stateOrProvinceName = State or Province Name (full name)
stateOrProvinceName_default = Queensland
localityName = Locality Name (eg, city)
localityName_default = Brisbane
0.organizationName = Organization Name (eg, company)
0.organizationName_default = INSECURE EXAMPLE
organizationalUnitName = Organizational Unit Name (eg, section)
organizationalUnitName_default = kanidm
commonName = Common Name (eg, your name or your server\'s hostname)
commonName_max = 64
commonName_default = insecure.ca.localhost
[ v3_ca ]
subjectKeyIdentifier = hash
basicConstraints = critical, CA:true
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
DEVEOF
cat > "${ALTNAME_FILE}" << DEVEOF
[ca]
default_ca = CA_default
[ CA_default ]
# Directory and file locations.
dir = ${KANI_TMP}
certs = ${KANI_TMP}
crl_dir = ${KANI_TMP}
new_certs_dir = ${KANI_TMP}
database = ${CADB}
serial = ${CASRL}
# The root key and root certificate.
private_key = ${CAKEY}
certificate = ${CACERT}
# SHA-1 is deprecated, so use SHA-2 instead.
default_md = sha256
name_opt = ca_default
cert_opt = ca_default
default_days = 3650
preserve = no
policy = policy_loose
[ policy_loose ]
countryName = optional
stateOrProvinceName = optional
localityName = optional
organizationName = optional
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
[req]
nsComment = "Certificate"
distinguished_name = req_distinguished_name
@ -54,14 +128,15 @@ organizationalUnitName_default = kanidm
commonName = Common Name (eg, your name or your server\'s hostname)
commonName_max = 64
commonName_default = localhost
commonName_default = ${CERT_HOSTNAME}
[ v3_req ]
# Extensions to add to a certificate request
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
nsCertType = server
nsComment = "Server Certificate"
subjectKeyIdentifier = hash
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
@ -70,34 +145,37 @@ IP.1 = 127.0.0.1
DEVEOF
# Make the ca
openssl req -x509 -new -newkey rsa:4096 -sha256 \
-keyout "${CAKEY}" \
-out "${CACERT}" \
-days +31 \
-subj "/C=AU/ST=Queensland/L=Brisbane/O=INSECURE/CN=insecure.ca.localhost" -nodes
touch ${CADB}
echo 1000 > ${CASRL}
# generate the ca private key
openssl genrsa -out "${KEYFILE}" 4096
# Make the ca key
openssl ecparam -genkey -name prime256v1 -noout -out "${CAKEY}"
# Self sign the CA.
openssl req -config "${CANAME_FILE}" \
-key "${CAKEY}" \
-new -x509 -days +31 \
-sha256 -extensions v3_ca \
-out "${CACERT}" \
-nodes
# generate the server private key
openssl ecparam -genkey -name prime256v1 -noout -out "${KEYFILE}"
# generate the certficate signing request
openssl req -sha256 \
-config "${ALTNAME_FILE}" \
-new -extensions v3_req \
openssl req -sha256 -new \
-config "${ALTNAME_FILE}" -extensions v3_req \
-key "${KEYFILE}"\
-subj "/C=AU/ST=Queensland/L=Brisbane/O=INSECURE/CN=${CERT_HOSTNAME}" \
-nodes \
-out "${CSRFILE}"
# sign the cert
openssl x509 -req -days 31 \
-extfile "${ALTNAME_FILE}" \
-CA "${CACERT}" \
-CAkey "${CAKEY}" \
-CAcreateserial \
openssl ca -config "${ALTNAME_FILE}" \
-extensions v3_req \
-days 31 -notext -md sha256 \
-in "${CSRFILE}" \
-out "${CERTFILE}" \
-extensions v3_req -sha256
-out "${CERTFILE}"
# Create the chain
cat "${CERTFILE}" "${CACERT}" > "${CHAINFILE}"
@ -107,4 +185,4 @@ openssl dhparam -in "${CAFILE}" -out "${DHFILE}" 2048
echo "Certificate chain is at: ${CHAINFILE}"
echo "Private key is at: ${KEYFILE}"
echo ""
echo "**Remember** the default action is to store the files in /tmp/ so they'll be deleted on reboot! Set the KANI_TMP environment variable before running this script if you want to change that. You'll need to update server config elsewhere if you do, however."
echo "**Remember** the default action is to store the files in /tmp/ so they'll be deleted on reboot! Set the KANI_TMP environment variable before running this script if you want to change that. You'll need to update server config elsewhere if you do, however."

View file

@ -15,16 +15,6 @@
- [The Recycle Bin](recycle_bin.md)
- [Why TLS?](why_tls.md)
# For Developers
- [Developer Guide](DEVELOPER_README.md)
- [Design Documents]()
- [Access Profiles](developers/designs/access_profiles_and_security.md)
- [REST Interface](developers/designs/rest_interface.md)
- [Python Module](developers/python.md)
- [RADIUS Integration](developers/radius.md)
# Integrations
- [Oauth2](integrations/oauth2.md)
@ -36,7 +26,17 @@
- [Kubernetes Ingress](examples/k8s_ingress_example.md)
# Packaging
# For Developers
- [Developer Guide](DEVELOPER_README.md)
- [Design Documents]()
- [Access Profiles](developers/designs/access_profiles_and_security.md)
- [REST Interface](developers/designs/rest_interface.md)
- [Python Module](developers/python.md)
- [RADIUS Integration](developers/radius.md)
## Packaging
- [Packaging](packaging.md)
- [Debian/Ubuntu](packaging_debs.md)
- [Debian/Ubuntu](packaging_debs.md)

View file

@ -3,92 +3,140 @@
Accounts and Groups are the primary reasons for Kanidm to exist. Kanidm is optimised as a repository
for these data. As a result, there are many concepts and important details to understand.
## Service Accounts vs Person Accounts
Kanidm seperates accounts into two types. Person accounts (or persons) are intended for use by humans
that will access the system in an interactive way. Service accounts are intended for use by computers
or services that need to identify themself to Kanidm. Generally a person or group of persons will
be responsible for and will manage service accounts. Because of this distinction these classes of
accounts have different properties and methods of authentication and management.
## Groups
Groups represent a collection of entities. This generally is a collection of persons or service accounts.
Groups are commonly used to assign privileges to the accouts that are members of a group. This allows
easier administration over larger systems where privileges can be assigned to groups in a logical
manner, and then only membership of the groups need administration, rather than needing to assign
privileges to each entity directly and uniquely.
Groups may also be nested, where a group can contain another group as a member. This allows hierarchies
to be created again for easier administration.
## Default Accounts and Groups
Kanidm ships with a number of default accounts and groups. This is to give you the best
out-of-box experience possible, as well as supplying best practice examples related to modern
Kanidm ships with a number of default service accounts and groups. This is to give you the best
out-of-box experience possible, as well as supplying best practice examples related to modern
Identity Management (IDM) systems.
The system administrator account has limited privileges (see
[Recovering the Initial idm_admin Account](#recovering-the-initial-idm_admin-account)) to learn
how to access the inbuilt admin account).
It manages only high-privilege accounts and services. This is to help separate system administration
from identity administration actions. An idm_admin user is also provided that is only for management
of accounts and groups.
There are two builtin system administration accounts.
Both the admin and the idm_admin user should *NOT* be used for daily activities - they exist for initial
`admin` is the default service account which has privileges to configure and administer kanidm as a whole.
This account can manage access controls, schema, integrations and more. However the `admin` can not
manage persons by default to seperate the priviliges. As this is a service account is is intended
for limited use.
`idm_admin` is the default service account which has privileges to create persons and to manage these
accounts and groups. They can perform credential resets and more.
Both the `admin` and the `idm_admin` user should *NOT* be used for daily activities - they exist for initial
system configuration, and for disaster recovery scenarios. You should delegate permissions
as required to named user accounts instead.
The majority of the provided content is privilege groups that provide rights over Kanidm
The majority of the builtin groups are privilige groups that provide rights over Kanidm
administrative actions. These include groups for account management, person management (personal
and sensitive data), group management, and more.
## Recovering the Initial idm_admin Account
## Recovering the Initial Admin Accounts
By default the idm_admin user has no password, and can not be accessed. You should recover it with the
admin (system admin) account.
By default the `admin` and `idm_admin` accounts have no password, and can not be accessed. They need
to be "recovered" from the server that is running the kanidmd server.
{{#template
{{#template
templates/kani-warning.md
imagepath=images
text=Warning: The server must not be running at this point, as it requires exclusive access to the database.
}}
We recommend the use of the "recover_account" functionality as it provides a high strength, random password.
```shell
kanidmd recover_account -c /etc/kanidm/server.toml -n idm_admin
Successfully recovered account 'idm_admin' - password reset to -> j9YUv...
kanidmd recover_account admin -c /etc/kanidm/server.toml
# Successfully recovered account 'admin' - password reset to -> j9YUv...
```
To do this in Docker, you'll need to stop the existing container and run it with `bash` as the "command" argument, to get a shell, then run the `kanidmd` command above.
For example, if I'm using the developer image in my test environment:
To do this with Docker, you'll need to stop the existing container and use the "command" argument to
access the kanidmd binary.
```shell
docker run --rm -it \
-v/tmp/kanidm:/data\
--name kanidmd \
--hostname kanidmd \
ghcr.io/kanidm/kanidmd:devel \
bash
kanidmd:/# kanidmd recover_account -c /data/server.toml -n idm_admin
Successfully recovered account 'idm_admin' - password reset to -> j9YUv...
kanidm/server:latest \
kanidmd recover_account admin -c /data/server.toml
```
Once that's done, exit the shell and start your server container again.
After the recovery is complete the server can be started again.
## Creating Accounts
You can now use the idm_admin user to create initial groups and accounts.
Once you have access to the admin account, it is able to reset the credentials of the `idm_admin`
account.
```shell
kanidm login --name idm_admin
kanidm group create demo_group --name idm_admin
kanidm account create demo_user "Demonstration User" --name idm_admin
kanidm group add_members demo_group demo_user --name idm_admin
kanidm group list_members demo_group --name idm_admin
kanidm account get demo_user --name idm_admin
kanidm login -D admin
kanidm service-account credential generate-pw -D admin idm_admin
# Success: wJX...
```
You can also use anonymous to view users and groups - note that you won't see as many fields due
to the limits of the anonymous access profile.
kanidm login --name anonymous
kanidm account get demo_user --name anonymous
These accounts will be used through the remainder of this document for managing the server.
## Viewing Default Groups
You should take some time to inspect the default groups which are related to
default permissions. These can be viewed with:
kanidm group list
kanidm group get <name>
```
kanidm group list
kanidm group get <name>
```
## Resetting Account Credentials
## Creating Person Accounts
Members of the `idm_account_manage_priv` group have the rights to manage other users'
By default `idm_admin` has the privileges to create new persons in the system.
```shell
kanidm login --name idm_admin
kanidm account create demo_user "Demonstration User" --name idm_admin
kanidm account get demo_user --name idm_admin
kanidm group create demo_group --name idm_admin
kanidm group add_members demo_group demo_user --name idm_admin
kanidm group list_members demo_group --name idm_admin
```
You can also use anonymous to view accounts and groups - note that you won't see certain fields due
to the limits of the access control anonymous access profile.
```
kanidm login --name anonymous
kanidm account get demo_user --name anonymous
```
Kanidm allows person accounts to include human related attributes, such as their legal name and email address.
Initially, a person does not have these attributes. If desired, a person may be modified to have these attributes.
```shell
# Note, both the --legalname and --mail flags may be omitted
kanidm account person update demo_user --legalname "initial name" --mail "initial@email.address"
```
{{#template
templates/kani-warning.md
imagepath=images
text=Warning: Persons may change their own displayname, name, and legal name at any time. You MUST NOT use these values as primary keys in external systems. You MUST use the `uuid` attribute present on all entries as an external primary key.
}}
## Resetting Person Account Credentials
Members of the `idm_account_manage_priv` group have the rights to manage person and service
accounts security and login aspects. This includes resetting account credentials.
You can perform a password reset on the demo_user, for example as the idm_admin user, who is
@ -118,6 +166,26 @@ kanidm login --name demo_user
kanidm self whoami --name demo_user
```
## Creating Service Accounts
The `admin` service account can be used to create service accounts.
```shell
kanidm service-account create demo_service "Demonstration Service" --name admin
kanidm service-account get demo_service --name admin
```
## Resetting Service Account Credentials
Service accounts can not have their credentials interactively updated in the same manner as
persons. Service accounts may only have server side generated high entropy passwords.
To re-generate this password to an account
```shell
kanidm service-account credential generate-pw demo_service --name admin
```
## Nested Groups
Kanidm supports groups being members of groups, allowing nested groups. These nesting relationships
@ -138,10 +206,8 @@ kanidm account get nest_example --name anonymous
## Account Validity
Kanidm supports accounts that are only able to be authenticated between specific date and time
date where authentication can succeed, and an expiry date where the account will no longer
windows. This takes the form of a "valid from" attribute that defines the earliest start
allow authentication.
Kanidm supports accounts that are only able authenticate between a pair of dates and times; the "valid
from" and "expires" timestamps define these points in time.
This can be displayed with:
@ -153,7 +219,15 @@ These datetimes are stored in the server as UTC, but presented according to your
to aid correct understanding of when the events will occur.
To set the values, an account with account management permission is required (for example, idm_admin).
Again, these values will correctly translated from the entered local timezone to UTC.
You may set these time and date values in any timezone you wish (such as your local timezone), and the
server will transform these to UTC. These time values are in iso8601 format, and you should specify this
as:
```
YYYY-MM-DDThh:mm:ssZ+-hh:mm
Year-Month-Day T hour:minutes:seconds Z +- timezone offset
```
Set the earliest time the account can start authenticating:
@ -181,30 +255,6 @@ where the "valid from" is *after* the expire_at, the expire_at will be respected
These validity settings impact all authentication functions of the account (kanidm, ldap, radius).
## People Accounts
Kanidm allows extending accounts to include additional "people" attributes,
such as their legal name and email address.
Initially, an account does not have these attributes. If desired, an account
may be modified to have these "person" attributes like so:
# Note, both the --legalname and --mail flags may be omitted
kanidm account person extend demo_user --legalname "initial name" --mail "initial@email.address"
Once an account has been extended, the "person" attributes may be set by the
user of the account, or anyone with enough privileges.
Whether an account is currently a "person" or not can be identified from the "account get" output:
kanidm account get demo_user
# ---
# class: person
# ... (other output omitted)
The presence of a "class: person" stanza indicates that this account may have
"people" attributes.
### Allowing people accounts to change their mail attribute
By default, Kanidm allows an account to change some attributes, but not their

View file

@ -66,8 +66,8 @@ Now you can check your instance is working. You may need to provide a CA certifi
with the -C parameter:
kanidm login --name anonymous
kanidm self whoami -C ../path/to/ca.pem -H https://localhost:8443 --name anonymous
kanidm self whoami -H https://localhost:8443 --name anonymous
kanidm self whoami -C ../path/to/ca.pem -H https://localhost:8443 --name anonymous
Now you can take some time to look at what commands are available - please
[ask for help at any time](https://github.com/kanidm/kanidm#getting-in-contact--questions).
Now you can take some time to look at what commands are available - please
[ask for help at any time](https://github.com/kanidm/kanidm#getting-in-contact--questions).

View file

@ -121,7 +121,6 @@ OR for a shell into the volume:
docker run --rm -i -t -v kanidmd:/data opensuse/leap:latest /bin/sh
# Continue on to [Configuring the Server](server_configuration.md)

View file

@ -293,8 +293,7 @@ oauth:
user_info_url: https://idm.wherekanidmruns.com/oauth2/openid/<oauth2_rs_name>/userinfo
```
The `email` scope needs to be passed and thus the attribute needs to exist in
the account:
The `email` scope needs to be passed and thus the mail attribute needs to exist on the account:
kanidm person update <ID> --mail "YYYY@somedomain.com" --name idm_admin
kanidm login --name idm_admin
kanidm account person extend YYYY --mail "YYYY@somedomain.com" --name idm_admin

View file

@ -16,23 +16,27 @@ It's worth noting some disclaimers about Kanidm's RADIUS integration.
### One Credential - One Account
Kanidm normally attempts to have credentials for each *device* and
Kanidm normally attempts to have credentials for each *device* and
*application* rather than the legacy model of one to one.
The RADIUS protocol is only able to attest a *single* credential in an
authentication attempt, which limits us to storing a single RADIUS credential
per account. However, despite this limitation, it still greatly improves the
situation by isolating the RADIUS credential from the primary or application
credentials of the account. This solves many common security concerns around
credential loss or disclosure, and prevents rogue devices from locking out
The RADIUS protocol is only able to attest a *single* password based credential in an
authentication attempt, which limits us to storing a single RADIUS password credential
per account. However, despite this limitation, it still greatly improves the
situation by isolating the RADIUS credential from the primary or application
credentials of the account. This solves many common security concerns around
credential loss or disclosure, and prevents rogue devices from locking out
accounts as they attempt to authenticate to Wi-Fi with expired credentials.
Alternatelly, Kanidm supports mapping users with special configuration of certificates
allowing some systems to use EAP-TLS for RADIUS authentication. This returns to the
"per device" credential model.
### Cleartext Credential Storage
RADIUS offers many different types of tunnels and authentication mechanisms.
However, most client devices "out of the box" only attempt a single type when
a WPA2-Enterprise network is selected: MSCHAPv2 with PEAP. This is a
challenge-response protocol that requires clear text or Windows NT LAN
However, most client devices "out of the box" only attempt a single type when
a WPA2-Enterprise network is selected: MSCHAPv2 with PEAP. This is a
challenge-response protocol that requires clear text or Windows NT LAN
Manager (NTLM) credentials.
As MSCHAPv2 with PEAP is the only practical, universal RADIUS-type supported
@ -41,8 +45,8 @@ that it MUST be supported as the default. Esoteric RADIUS types can be used
as well, but this is up to administrators to test and configure.
Due to this requirement, we must store the RADIUS material as clear text or
NTLM hashes. It would be silly to think that NTLM is secure as it relies on
the obsolete and deprecated MD4 cryptographic hash, providing only an
NTLM hashes. It would be silly to think that NTLM is secure as it relies on
the obsolete and deprecated MD4 cryptographic hash, providing only an
illusion of security.
This means Kanidm stores RADIUS credentials in the database as clear text.
@ -51,19 +55,25 @@ We believe this is a reasonable decision and is a low risk to security because:
* The access controls around RADIUS secrets by default are strong, limited
to only self-account read and RADIUS-server read.
* As RADIUS credentials are separate from the primary account credentials and
have no other rights, their disclosure is not going to lead to a full
* As RADIUS credentials are separate from the primary account credentials and
have no other rights, their disclosure is not going to lead to a full
account compromise.
* Having the credentials in clear text allows a better user experience as
* Having the credentials in clear text allows a better user experience as
clients can view the credentials at any time to enroll further devices.
### Service Accounts Do Not Have Radius Access
Due to the design of service accounts, they do not have access to radius for credential assignemnt.
If you require RADIUS usage with a service account you *may* need to use EAP-TLS or some other
authentication method.
## Account Credential Configuration
For an account to use RADIUS they must first generate a RADIUS secret unique
to that account. By default, all accounts can self-create this secret.
kanidm account radius generate_secret --name william william
kanidm account radius show_secret --name william william
kanidm person radius generate_secret --name william william
kanidm person radius show_secret --name william william
## Account Group Configuration
@ -82,17 +92,17 @@ To read these secrets, the RADIUS server requires an account with the
correct privileges. This can be created and assigned through the group
"idm_radius_servers", which is provided by default.
First, create the account and add it to the group:
First, create the service account and add it to the group:
```shell
kanidm account create --name admin radius_service_account "Radius Service Account"
kanidm service-account create --name admin radius_service_account "Radius Service Account"
kanidm group add_members --name admin idm_radius_servers radius_service_account
```
Now reset the account password, using the `admin` account:
```shell
kanidm account credential update --name admin radius_service_account
kanidm service-account credential generate-pw --name admin radius_service_account
```
## Deploying a RADIUS Container

View file

@ -10,8 +10,8 @@ The intent of the Kanidm project is to:
* Make system, network, application and web authentication easy and accessible.
{{#template
templates/kani-warning.md
{{#template
templates/kani-warning.md
imagepath=images
title=NOTICE
text=This is a pre-release project. While all effort has been made to ensure no data loss or security flaws, you should still be careful when using this in your environment.
@ -23,8 +23,8 @@ Looking for the `rustdoc` documentation for the libraries themselves? [Click her
## Why do I want Kanidm?
Whether you work in a business, a volunteer organisation, or are an enthusiast who manages
their personal services, you need methods of authenticating and identifying
Whether you work in a business, a volunteer organisation, or are an enthusiast who manages
their personal services, you need methods of authenticating and identifying
to your systems, and subsequently, ways to determine what authorisation and privileges you have
while accessing these systems.

View file

@ -1,7 +1,8 @@
# POSIX Accounts and Groups
Kanidm has features that enable its accounts and groups to be consumed on
POSIX-like machines, such as Linux, FreeBSD, or others.
POSIX-like machines, such as Linux, FreeBSD, or others. Both service accounts
and person accounts can be used on POSIX systems.
## Notes on POSIX Features
@ -38,8 +39,9 @@ two independent items. For example in /etc/passwd and /etc/group:
# group
william:x:654401105:
Other systems like FreeIPA use a plugin that generates a UPG as a database record on
creation of the account.
Other systems like FreeIPA use a plugin that generates a UPG as a seperate group entry on
creation of the account. This means there are two entries for an account, and they must
be kept in lock-step.
Kanidm does neither of these. As the GID number of the user must be unique, and a user
implies the UPG must exist, we can generate UPG's on-demand from the account.
@ -48,19 +50,18 @@ UPG - given the nature of a user private group, this is the point.
### GID Number Generation
In the future, Kanidm plans to have asynchronous replication as a feature between writable
Kanidm will have asynchronous replication as a feature between writable
database servers. In this case, we need to be able to allocate stable and reliable
GID numbers to accounts on replicas that may not be in continual communication.
To do this, we use the last 32 bits of the account or group's UUID to
generate the GID number.
To do this, we use the last 32 bits of the account or group's UUID to generate the GID number.
A valid concern is the possibility of duplication in the lower 32 bits. Given the
birthday problem, if you have 77,000 groups and accounts, you have a 50% chance
of duplication. With 50,000 you have a 20% chance, 9,300 you have a 1% chance and
with 2900 you have a 0.1% chance.
We advise that if you have a site with >10,000 users you should use an external system
We advise that if you have a site with >10,000 users you should use an external system
to allocate GID numbers serially or consistently to avoid potential duplication events.
This design decision is made as most small sites will benefit greatly from the
@ -72,19 +73,25 @@ capable of supplying this kind of data in batch jobs.
### Enabling POSIX Attributes on Accounts
To enable POSIX account features and IDs on an account, you require the permission
To enable POSIX account features and IDs on an account, you require the permission
`idm_account_unix_extend_priv`. This is provided to `idm_admins` in the default database.
You can then use the following command to enable POSIX extensions.
You can then use the following command to enable POSIX extensions on a person or service account.
kanidm account posix set --name idm_admin <account_id> [--shell SHELL --gidnumber GID]
kanidm account posix set --name idm_admin demo_user
kanidm account posix set --name idm_admin demo_user --shell /bin/zsh
kanidm account posix set --name idm_admin demo_user --gidnumber 2001
kanidm [person OR service-account] posix set --name idm_admin <account_id> [--shell SHELL --gidnumber GID]
kanidm person posix set --name idm_admin demo_user
kanidm person posix set --name idm_admin demo_user --shell /bin/zsh
kanidm person posix set --name idm_admin demo_user --gidnumber 2001
kanidm service-account posix set --name idm_admin demo_account
kanidm service-account posix set --name idm_admin demo_account --shell /bin/zsh
kanidm service-account posix set --name idm_admin demo_account --gidnumber 2001
You can view the accounts POSIX token details with:
kanidm account posix show --name anonymous demo_user
kanidm person posix show --name anonymous demo_user
kanidm service-account posix show --name anonymous demo_account
### Enabling POSIX Attributes on Groups
@ -115,4 +122,4 @@ with Kanidm accounts may fail with an error such as:
This is a fault in Podman and how it attempts to provide non-root containers, when UID/GIDs
are greater than 65535. In this case you may manually allocate your users GID number to be
between 1000 - 65535, which may not trigger the fault.
between 1000 - 65535, which may not trigger the fault.

View file

@ -29,15 +29,15 @@ Currently they stay up to 1 week before they are removed.
You can display all items in the Recycle Bin with:
kanidm recycle_bin list --name admin
kanidm recycle-bin list --name admin
You can show a single items with:
You can show a single item with:
kanidm recycle_bin get --name admin <id>
kanidm recycle-bin get --name admin <uuid>
An entry can be revived with:
kanidm recycle_bin revive --name admin <id>
kanidm recycle-bin revive --name admin <uuid>
## Edge Cases
@ -71,4 +71,4 @@ These issues could be looked at again in the future, but for now we think that d
groups is rare - we expect recycle bin to save you in "opps" moments, and in a majority
of cases you may delete a group or a user and then restore them. To handle this series
of steps requires extra code complexity in how we flag operations. For more,
see [This issue on github](https://github.com/kanidm/kanidm/issues/177).
see [This issue on github](https://github.com/kanidm/kanidm/issues/177).

View file

@ -104,7 +104,7 @@ You need a configuration file in the volume named `server.toml`. (Within the con
An example is located in [examples/server.toml](https://github.com/kanidm/kanidm/blob/master/examples/server.toml).
{{#template
{{#template
templates/kani-warning.md
imagepath=images
title=Warning!

View file

@ -1,33 +1,33 @@
# SSH Key Distribution
To support SSH authentication securely to a large set of hosts running SSH, we support
distribution of SSH public keys via the Kanidm server.
To support SSH authentication securely to a large set of hosts running SSH, we support
distribution of SSH public keys via the Kanidm server. Both persons and service accounts
support SSH public keys on their accounts.
## Configuring Accounts
To view the current SSH public keys on accounts, you can use:
kanidm account ssh list_publickeys --name <login user> <account to view>
kanidm account ssh list_publickeys --name idm_admin william
kanidm person|service-account ssh list_publickeys --name <login user> <account to view>
kanidm person|service-account ssh list_publickeys --name idm_admin william
All users by default can self-manage their SSH public keys. To upload a key, a command like this
is the best way to do so:
kanidm account ssh add_publickey --name william william 'test-key' "`cat ~/.ssh/id_rsa.pub`"
kanidm person|service-account ssh add_publickey --name william william 'test-key' "`cat ~/.ssh/id_rsa.pub`"
To remove (revoke) an SSH public key, delete them by the tag name:
kanidm account ssh delete_publickey --name william william 'test-key'
kanidm person|service-account ssh delete_publickey --name william william 'test-key'
## Security Notes
As a security feature, Kanidm validates *all* public keys to ensure they are valid SSH public keys.
Uploading a private key or other data will be rejected. For example:
kanidm account ssh add_publickey --name william william 'test-key' "invalid"
kanidm person|service-account ssh add_publickey --name william william 'test-key' "invalid"
Enter password:
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value:
Http(400, Some(SchemaViolation(InvalidAttributeSyntax)))', src/libcore/result.rs:1084:5
... Some(SchemaViolation(InvalidAttributeSyntax)))' ...
## Server Configuration
@ -101,4 +101,4 @@ contain the lines:
Restart sshd, and then attempt to authenticate with the keys.
It's highly recommended you keep your client configuration and sshd_configuration in a configuration
management tool such as salt or ansible.
management tool such as salt or ansible.

View file

@ -42,6 +42,9 @@ use webauthn_rs_proto::{
PublicKeyCredential, RegisterPublicKeyCredential, RequestChallengeResponse,
};
mod person;
mod service_account;
pub const APPLICATION_JSON: &str = "application/json";
pub const KOPID: &str = "X-KANIDM-OPID";
pub const KSESSIONID: &str = "X-KANIDM-AUTH-SESSION-ID";
@ -1300,8 +1303,6 @@ impl KanidmClient {
}
pub async fn idm_group_unix_token_get(&self, id: &str) -> Result<UnixGroupToken, ClientError> {
// Format doesn't work in async
// format!("/v1/account/{}/_unix/_token", id).as_str()
self.perform_get_request(["/v1/group/", id, "/_unix/_token"].concat().as_str())
.await
}
@ -1312,33 +1313,6 @@ impl KanidmClient {
}
// ==== ACCOUNTS
pub async fn idm_account_list(&self) -> Result<Vec<Entry>, ClientError> {
self.perform_get_request("/v1/account").await
}
pub async fn idm_account_create(&self, name: &str, dn: &str) -> Result<(), ClientError> {
let mut new_acct = Entry {
attrs: BTreeMap::new(),
};
new_acct
.attrs
.insert("name".to_string(), vec![name.to_string()]);
new_acct
.attrs
.insert("displayname".to_string(), vec![dn.to_string()]);
self.perform_post_request("/v1/account", new_acct).await
}
pub async fn idm_account_set_password(&self, cleartext: String) -> Result<(), ClientError> {
let s = SingleStringRequest { value: cleartext };
self.perform_post_request("/v1/self/_credential/primary/set_password", s)
.await
}
pub async fn idm_account_set_displayname(&self, id: &str, dn: &str) -> Result<(), ClientError> {
self.idm_account_set_attr(id, "displayname", &[dn]).await
}
pub async fn idm_account_unix_token_get(&self, id: &str) -> Result<UnixUserToken, ClientError> {
// Format doesn't work in async
@ -1347,293 +1321,12 @@ impl KanidmClient {
.await
}
pub async fn idm_account_delete(&self, id: &str) -> Result<(), ClientError> {
self.perform_delete_request(["/v1/account/", id].concat().as_str())
.await
}
pub async fn idm_account_get(&self, id: &str) -> Result<Option<Entry>, ClientError> {
self.perform_get_request(format!("/v1/account/{}", id).as_str())
.await
}
pub async fn idm_account_add_attr(
&self,
id: &str,
attr: &str,
values: &[&str],
) -> Result<(), ClientError> {
let msg: Vec<_> = values.iter().map(|v| (*v).to_string()).collect();
self.perform_post_request(format!("/v1/account/{}/_attr/{}", id, attr).as_str(), msg)
.await
}
pub async fn idm_account_set_attr(
&self,
id: &str,
attr: &str,
values: &[&str],
) -> Result<(), ClientError> {
let m: Vec<_> = values.iter().map(|v| (*v).to_string()).collect();
self.perform_put_request(format!("/v1/account/{}/_attr/{}", id, attr).as_str(), m)
.await
}
pub async fn idm_account_get_attr(
&self,
id: &str,
attr: &str,
) -> Result<Option<Vec<String>>, ClientError> {
self.perform_get_request(format!("/v1/account/{}/_attr/{}", id, attr).as_str())
.await
}
pub async fn idm_account_purge_attr(&self, id: &str, attr: &str) -> Result<(), ClientError> {
self.perform_delete_request(format!("/v1/account/{}/_attr/{}", id, attr).as_str())
.await
}
pub async fn idm_account_primary_credential_set_password(
&self,
id: &str,
pw: &str,
) -> Result<SetCredentialResponse, ClientError> {
let r = SetCredentialRequest::Password(pw.to_string());
self.perform_put_request(
format!("/v1/account/{}/_credential/primary", id).as_str(),
r,
)
.await
}
pub async fn idm_account_primary_credential_import_password(
&self,
id: &str,
pw: &str,
) -> Result<(), ClientError> {
self.perform_put_request(
format!("/v1/account/{}/_attr/password_import", id).as_str(),
vec![pw.to_string()],
)
.await
}
pub async fn idm_account_primary_credential_set_generated(
&self,
id: &str,
) -> Result<String, ClientError> {
let r = SetCredentialRequest::GeneratePassword;
let res: Result<SetCredentialResponse, ClientError> = self
.perform_put_request(
format!("/v1/account/{}/_credential/primary", id).as_str(),
r,
)
.await;
match res {
Ok(SetCredentialResponse::Token(p)) => Ok(p),
Ok(_) => Err(ClientError::EmptyResponse),
Err(e) => Err(e),
}
}
// Reg intent for totp
pub async fn idm_account_primary_credential_generate_totp(
&self,
id: &str,
) -> Result<(Uuid, TotpSecret), ClientError> {
let r = SetCredentialRequest::TotpGenerate;
let res: Result<SetCredentialResponse, ClientError> = self
.perform_put_request(
format!("/v1/account/{}/_credential/primary", id).as_str(),
r,
)
.await;
match res {
Ok(SetCredentialResponse::TotpCheck(u, s)) => Ok((u, s)),
Ok(_) => Err(ClientError::EmptyResponse),
Err(e) => Err(e),
}
}
// Verify the totp
pub async fn idm_account_primary_credential_verify_totp(
&self,
id: &str,
otp: u32,
session: Uuid,
) -> Result<(), ClientError> {
let r = SetCredentialRequest::TotpVerify(session, otp);
let res: Result<SetCredentialResponse, ClientError> = self
.perform_put_request(
format!("/v1/account/{}/_credential/primary", id).as_str(),
r,
)
.await;
match res {
Ok(SetCredentialResponse::Success) => Ok(()),
Ok(SetCredentialResponse::TotpCheck(u, s)) => Err(ClientError::TotpVerifyFailed(u, s)),
Ok(SetCredentialResponse::TotpInvalidSha1(u)) => Err(ClientError::TotpInvalidSha1(u)),
Ok(_) => Err(ClientError::EmptyResponse),
Err(e) => Err(e),
}
}
// Accept a sha1 totp
pub async fn idm_account_primary_credential_accept_sha1_totp(
&self,
id: &str,
session: Uuid,
) -> Result<(), ClientError> {
let r = SetCredentialRequest::TotpAcceptSha1(session);
let res: Result<SetCredentialResponse, ClientError> = self
.perform_put_request(
format!("/v1/account/{}/_credential/primary", id).as_str(),
r,
)
.await;
match res {
Ok(SetCredentialResponse::Success) => Ok(()),
Ok(_) => Err(ClientError::EmptyResponse),
Err(e) => Err(e),
}
}
pub async fn idm_account_primary_credential_remove_totp(
&self,
id: &str,
) -> Result<(), ClientError> {
let r = SetCredentialRequest::TotpRemove;
let res: Result<SetCredentialResponse, ClientError> = self
.perform_put_request(
format!("/v1/account/{}/_credential/primary", id).as_str(),
r,
)
.await;
match res {
Ok(SetCredentialResponse::Success) => Ok(()),
Ok(_) => Err(ClientError::EmptyResponse),
Err(e) => Err(e),
}
}
/*
pub async fn idm_account_primary_credential_register_webauthn(
&self,
id: &str,
label: &str,
) -> Result<(Uuid, CreationChallengeResponse), ClientError> {
let r = SetCredentialRequest::WebauthnBegin(label.to_string());
let res: Result<SetCredentialResponse, ClientError> = self
.perform_put_request(
format!("/v1/account/{}/_credential/primary", id).as_str(),
r,
)
.await;
match res {
Ok(SetCredentialResponse::WebauthnCreateChallenge(u, s)) => Ok((u, s)),
Ok(_) => Err(ClientError::EmptyResponse),
Err(e) => Err(e),
}
}
pub async fn idm_account_primary_credential_complete_webuthn_registration(
&self,
id: &str,
rego: RegisterPublicKeyCredential,
session: Uuid,
) -> Result<(), ClientError> {
let r = SetCredentialRequest::WebauthnRegister(session, rego);
let res: Result<SetCredentialResponse, ClientError> = self
.perform_put_request(
format!("/v1/account/{}/_credential/primary", id).as_str(),
r,
)
.await;
match res {
Ok(SetCredentialResponse::Success) => Ok(()),
Ok(_) => Err(ClientError::EmptyResponse),
Err(e) => Err(e),
}
}
pub async fn idm_account_primary_credential_remove_webauthn(
&self,
id: &str,
label: &str,
) -> Result<(), ClientError> {
let r = SetCredentialRequest::WebauthnRemove(label.to_string());
let res: Result<SetCredentialResponse, ClientError> = self
.perform_put_request(
format!("/v1/account/{}/_credential/primary", id).as_str(),
r,
)
.await;
match res {
Ok(SetCredentialResponse::Success) => Ok(()),
Ok(_) => Err(ClientError::EmptyResponse),
Err(e) => Err(e),
}
}
*/
pub async fn idm_account_primary_credential_generate_backup_code(
&self,
id: &str,
) -> Result<Vec<String>, ClientError> {
let r = SetCredentialRequest::BackupCodeGenerate;
let res: Result<SetCredentialResponse, ClientError> = self
.perform_put_request(
format!("/v1/account/{}/_credential/primary", id).as_str(),
r,
)
.await;
match res {
Ok(SetCredentialResponse::BackupCodes(s)) => Ok(s),
Ok(_) => Err(ClientError::EmptyResponse),
Err(e) => Err(e),
}
}
pub async fn idm_account_primary_credential_remove_backup_code(
&self,
id: &str,
) -> Result<(), ClientError> {
let r = SetCredentialRequest::BackupCodeRemove;
let res: Result<SetCredentialResponse, ClientError> = self
.perform_put_request(
format!("/v1/account/{}/_credential/primary", id).as_str(),
r,
)
.await;
match res {
Ok(SetCredentialResponse::Success) => Ok(()),
Ok(_) => Err(ClientError::EmptyResponse),
Err(e) => Err(e),
}
}
pub async fn idm_account_get_credential_status(
&self,
id: &str,
) -> Result<CredentialStatus, ClientError> {
let res: Result<CredentialStatus, ClientError> = self
.perform_get_request(format!("/v1/account/{}/_credential/_status", id).as_str())
.await;
res.and_then(|cs| {
if cs.creds.is_empty() {
Err(ClientError::EmptyResponse)
} else {
Ok(cs)
}
})
}
// == new credential update session code.
pub async fn idm_account_credential_update_intent(
pub async fn idm_person_account_credential_update_intent(
&self,
id: &str,
) -> Result<CUIntentToken, ClientError> {
self.perform_get_request(format!("/v1/account/{}/_credential/_update_intent", id).as_str())
self.perform_get_request(format!("/v1/person/{}/_credential/_update_intent", id).as_str())
.await
}
@ -1641,7 +1334,7 @@ impl KanidmClient {
&self,
id: &str,
) -> Result<(CUSessionToken, CUStatus), ClientError> {
self.perform_get_request(format!("/v1/account/{}/_credential/_update", id).as_str())
self.perform_get_request(format!("/v1/person/{}/_credential/_update", id).as_str())
.await
}
@ -1774,26 +1467,7 @@ impl KanidmClient {
.await
}
pub async fn idm_account_radius_credential_get(
&self,
id: &str,
) -> Result<Option<String>, ClientError> {
self.perform_get_request(format!("/v1/account/{}/_radius", id).as_str())
.await
}
pub async fn idm_account_radius_credential_regenerate(
&self,
id: &str,
) -> Result<String, ClientError> {
self.perform_post_request(format!("/v1/account/{}/_radius", id).as_str(), ())
.await
}
pub async fn idm_account_radius_credential_delete(&self, id: &str) -> Result<(), ClientError> {
self.perform_delete_request(format!("/v1/account/{}/_radius", id).as_str())
.await
}
// == radius
pub async fn idm_account_radius_token_get(
&self,
@ -1803,36 +1477,6 @@ impl KanidmClient {
.await
}
pub async fn idm_account_unix_extend(
&self,
id: &str,
gidnumber: Option<u32>,
shell: Option<&str>,
) -> Result<(), ClientError> {
let ux = AccountUnixExtend {
shell: shell.map(str::to_string),
gidnumber,
};
self.perform_post_request(format!("/v1/account/{}/_unix", id).as_str(), ux)
.await
}
pub async fn idm_account_unix_cred_put(&self, id: &str, cred: &str) -> Result<(), ClientError> {
let req = SingleStringRequest {
value: cred.to_string(),
};
self.perform_put_request(
["/v1/account/", id, "/_unix/_credential"].concat().as_str(),
req,
)
.await
}
pub async fn idm_account_unix_cred_delete(&self, id: &str) -> Result<(), ClientError> {
self.perform_delete_request(["/v1/account/", id, "/_unix/_credential"].concat().as_str())
.await
}
pub async fn idm_account_unix_cred_verify(
&self,
id: &str,
@ -1845,64 +1489,7 @@ impl KanidmClient {
.await
}
/*
pub async fn idm_account_orgperson_extend(
&self,
id: &str,
mail: &str,
) -> Result<(), ClientError> {
let x = AccountOrgPersonExtend {
mail: mail.to_string(),
};
self.perform_post_request(format!("/v1/account/{}/_orgperson", id).as_str(), x)
.await
}
*/
pub async fn idm_account_get_ssh_pubkeys(&self, id: &str) -> Result<Vec<String>, ClientError> {
self.perform_get_request(format!("/v1/account/{}/_ssh_pubkeys", id).as_str())
.await
}
pub async fn idm_account_post_ssh_pubkey(
&self,
id: &str,
tag: &str,
pubkey: &str,
) -> Result<(), ClientError> {
let sk = (tag.to_string(), pubkey.to_string());
self.perform_post_request(format!("/v1/account/{}/_ssh_pubkeys", id).as_str(), sk)
.await
}
pub async fn idm_account_person_extend(
&self,
id: &str,
mail: Option<&[String]>,
legalname: Option<&str>,
) -> Result<(), ClientError> {
let px = AccountPersonSet {
mail: mail.map(|s| s.to_vec()),
legalname: legalname.map(str::to_string),
};
self.perform_post_request(format!("/v1/account/{}/_person/_extend", id).as_str(), px)
.await
}
pub async fn idm_account_person_set(
&self,
id: &str,
mail: Option<&[String]>,
legalname: Option<&str>,
) -> Result<(), ClientError> {
let px = AccountPersonSet {
mail: mail.map(|s| s.to_vec()),
legalname: legalname.map(str::to_string),
};
self.perform_post_request(format!("/v1/account/{}/_person/_set", id).as_str(), px)
.await
}
// == generic ssh key handlers
pub async fn idm_account_get_ssh_pubkey(
&self,
id: &str,
@ -1912,12 +1499,8 @@ impl KanidmClient {
.await
}
pub async fn idm_account_delete_ssh_pubkey(
&self,
id: &str,
tag: &str,
) -> Result<(), ClientError> {
self.perform_delete_request(format!("/v1/account/{}/_ssh_pubkeys/{}", id, tag).as_str())
pub async fn idm_account_get_ssh_pubkeys(&self, id: &str) -> Result<Vec<String>, ClientError> {
self.perform_get_request(format!("/v1/account/{}/_ssh_pubkeys", id).as_str())
.await
}

236
kanidm_client/src/person.rs Normal file
View file

@ -0,0 +1,236 @@
use crate::ClientError;
use crate::KanidmClient;
use kanidm_proto::v1::AccountUnixExtend;
use kanidm_proto::v1::CredentialStatus;
use kanidm_proto::v1::Entry;
use kanidm_proto::v1::SingleStringRequest;
use std::collections::BTreeMap;
impl KanidmClient {
pub async fn idm_person_account_list(&self) -> Result<Vec<Entry>, ClientError> {
self.perform_get_request("/v1/person").await
}
pub async fn idm_person_account_get(&self, id: &str) -> Result<Option<Entry>, ClientError> {
self.perform_get_request(format!("/v1/person/{}", id).as_str())
.await
}
pub async fn idm_person_account_create(
&self,
name: &str,
displayname: &str,
) -> Result<(), ClientError> {
let mut new_acct = Entry {
attrs: BTreeMap::new(),
};
new_acct
.attrs
.insert("name".to_string(), vec![name.to_string()]);
new_acct
.attrs
.insert("displayname".to_string(), vec![displayname.to_string()]);
self.perform_post_request("/v1/person", new_acct).await
}
pub async fn idm_person_account_update(
&self,
id: &str,
newname: Option<&str>,
displayname: Option<&str>,
legalname: Option<&str>,
mail: Option<&[String]>,
) -> Result<(), ClientError> {
let mut update_entry = Entry {
attrs: BTreeMap::new(),
};
if let Some(newname) = newname {
update_entry
.attrs
.insert("name".to_string(), vec![newname.to_string()]);
}
if let Some(newdisplayname) = displayname {
update_entry
.attrs
.insert("displayname".to_string(), vec![newdisplayname.to_string()]);
}
if let Some(newlegalname) = legalname {
update_entry
.attrs
.insert("legalname".to_string(), vec![newlegalname.to_string()]);
}
if let Some(mail) = mail {
update_entry.attrs.insert("mail".to_string(), mail.to_vec());
}
self.perform_patch_request(format!("/v1/person/{}", id).as_str(), update_entry)
.await
}
pub async fn idm_person_account_delete(&self, id: &str) -> Result<(), ClientError> {
self.perform_delete_request(["/v1/person/", id].concat().as_str())
.await
}
pub async fn idm_person_account_add_attr(
&self,
id: &str,
attr: &str,
values: &[&str],
) -> Result<(), ClientError> {
let msg: Vec<_> = values.iter().map(|v| (*v).to_string()).collect();
self.perform_post_request(format!("/v1/person/{}/_attr/{}", id, attr).as_str(), msg)
.await
}
pub async fn idm_person_account_set_attr(
&self,
id: &str,
attr: &str,
values: &[&str],
) -> Result<(), ClientError> {
let m: Vec<_> = values.iter().map(|v| (*v).to_string()).collect();
self.perform_put_request(format!("/v1/person/{}/_attr/{}", id, attr).as_str(), m)
.await
}
pub async fn idm_person_account_get_attr(
&self,
id: &str,
attr: &str,
) -> Result<Option<Vec<String>>, ClientError> {
self.perform_get_request(format!("/v1/person/{}/_attr/{}", id, attr).as_str())
.await
}
pub async fn idm_person_account_purge_attr(
&self,
id: &str,
attr: &str,
) -> Result<(), ClientError> {
self.perform_delete_request(format!("/v1/person/{}/_attr/{}", id, attr).as_str())
.await
}
pub async fn idm_person_account_primary_credential_import_password(
&self,
id: &str,
pw: &str,
) -> Result<(), ClientError> {
self.perform_put_request(
format!("/v1/person/{}/_attr/password_import", id).as_str(),
vec![pw.to_string()],
)
.await
}
pub async fn idm_person_account_get_credential_status(
&self,
id: &str,
) -> Result<CredentialStatus, ClientError> {
let res: Result<CredentialStatus, ClientError> = self
.perform_get_request(format!("/v1/person/{}/_credential/_status", id).as_str())
.await;
res.and_then(|cs| {
if cs.creds.is_empty() {
Err(ClientError::EmptyResponse)
} else {
Ok(cs)
}
})
}
// This helper calls through the credential update session wrappers to
pub async fn idm_person_account_primary_credential_set_password(
&self,
id: &str,
pw: &str,
) -> Result<(), ClientError> {
let (session_tok, status) = self.idm_account_credential_update_begin(id).await?;
trace!(?status);
let status = self
.idm_account_credential_update_set_password(&session_tok, pw)
.await?;
trace!(?status);
self.idm_account_credential_update_commit(&session_tok)
.await
}
pub async fn idm_person_account_post_ssh_pubkey(
&self,
id: &str,
tag: &str,
pubkey: &str,
) -> Result<(), ClientError> {
let sk = (tag.to_string(), pubkey.to_string());
self.perform_post_request(format!("/v1/person/{}/_ssh_pubkeys", id).as_str(), sk)
.await
}
pub async fn idm_person_account_delete_ssh_pubkey(
&self,
id: &str,
tag: &str,
) -> Result<(), ClientError> {
self.perform_delete_request(format!("/v1/person/{}/_ssh_pubkeys/{}", id, tag).as_str())
.await
}
pub async fn idm_person_account_unix_extend(
&self,
id: &str,
gidnumber: Option<u32>,
shell: Option<&str>,
) -> Result<(), ClientError> {
let ux = AccountUnixExtend {
shell: shell.map(str::to_string),
gidnumber,
};
self.perform_post_request(format!("/v1/person/{}/_unix", id).as_str(), ux)
.await
}
pub async fn idm_person_account_unix_cred_put(
&self,
id: &str,
cred: &str,
) -> Result<(), ClientError> {
let req = SingleStringRequest {
value: cred.to_string(),
};
self.perform_put_request(
["/v1/person/", id, "/_unix/_credential"].concat().as_str(),
req,
)
.await
}
pub async fn idm_person_account_unix_cred_delete(&self, id: &str) -> Result<(), ClientError> {
self.perform_delete_request(["/v1/person/", id, "/_unix/_credential"].concat().as_str())
.await
}
pub async fn idm_account_radius_credential_get(
&self,
id: &str,
) -> Result<Option<String>, ClientError> {
self.perform_get_request(format!("/v1/person/{}/_radius", id).as_str())
.await
}
pub async fn idm_account_radius_credential_regenerate(
&self,
id: &str,
) -> Result<String, ClientError> {
self.perform_post_request(format!("/v1/person/{}/_radius", id).as_str(), ())
.await
}
pub async fn idm_account_radius_credential_delete(&self, id: &str) -> Result<(), ClientError> {
self.perform_delete_request(format!("/v1/person/{}/_radius", id).as_str())
.await
}
}

View file

@ -0,0 +1,196 @@
use crate::ClientError;
use crate::KanidmClient;
use kanidm_proto::v1::AccountUnixExtend;
use kanidm_proto::v1::CredentialStatus;
use kanidm_proto::v1::Entry;
use std::collections::BTreeMap;
impl KanidmClient {
pub async fn idm_service_account_list(&self) -> Result<Vec<Entry>, ClientError> {
self.perform_get_request("/v1/service_account").await
}
pub async fn idm_service_account_get(&self, id: &str) -> Result<Option<Entry>, ClientError> {
self.perform_get_request(format!("/v1/service_account/{}", id).as_str())
.await
}
pub async fn idm_service_account_create(
&self,
name: &str,
displayname: &str,
) -> Result<(), ClientError> {
let mut new_acct = Entry {
attrs: BTreeMap::new(),
};
new_acct
.attrs
.insert("name".to_string(), vec![name.to_string()]);
new_acct
.attrs
.insert("displayname".to_string(), vec![displayname.to_string()]);
self.perform_post_request("/v1/service_account", new_acct)
.await
}
pub async fn idm_service_account_delete(&self, id: &str) -> Result<(), ClientError> {
self.perform_delete_request(["/v1/service_account/", id].concat().as_str())
.await
}
pub async fn idm_service_account_update(
&self,
id: &str,
newname: Option<&str>,
displayname: Option<&str>,
mail: Option<&[String]>,
) -> Result<(), ClientError> {
let mut update_entry = Entry {
attrs: BTreeMap::new(),
};
if let Some(newname) = newname {
update_entry
.attrs
.insert("name".to_string(), vec![newname.to_string()]);
}
if let Some(newdisplayname) = displayname {
update_entry
.attrs
.insert("displayname".to_string(), vec![newdisplayname.to_string()]);
}
if let Some(mail) = mail {
update_entry.attrs.insert("mail".to_string(), mail.to_vec());
}
self.perform_patch_request(format!("/v1/service_account/{}", id).as_str(), update_entry)
.await
}
pub async fn idm_service_account_add_attr(
&self,
id: &str,
attr: &str,
values: &[&str],
) -> Result<(), ClientError> {
let msg: Vec<_> = values.iter().map(|v| (*v).to_string()).collect();
self.perform_post_request(
format!("/v1/service_account/{}/_attr/{}", id, attr).as_str(),
msg,
)
.await
}
pub async fn idm_service_account_set_attr(
&self,
id: &str,
attr: &str,
values: &[&str],
) -> Result<(), ClientError> {
let m: Vec<_> = values.iter().map(|v| (*v).to_string()).collect();
self.perform_put_request(
format!("/v1/service_account/{}/_attr/{}", id, attr).as_str(),
m,
)
.await
}
pub async fn idm_service_account_get_attr(
&self,
id: &str,
attr: &str,
) -> Result<Option<Vec<String>>, ClientError> {
self.perform_get_request(format!("/v1/service_account/{}/_attr/{}", id, attr).as_str())
.await
}
pub async fn idm_service_account_purge_attr(
&self,
id: &str,
attr: &str,
) -> Result<(), ClientError> {
self.perform_delete_request(format!("/v1/service_account/{}/_attr/{}", id, attr).as_str())
.await
}
pub async fn idm_service_account_post_ssh_pubkey(
&self,
id: &str,
tag: &str,
pubkey: &str,
) -> Result<(), ClientError> {
let sk = (tag.to_string(), pubkey.to_string());
self.perform_post_request(
format!("/v1/service_account/{}/_ssh_pubkeys", id).as_str(),
sk,
)
.await
}
pub async fn idm_service_account_delete_ssh_pubkey(
&self,
id: &str,
tag: &str,
) -> Result<(), ClientError> {
self.perform_delete_request(
format!("/v1/service_account/{}/_ssh_pubkeys/{}", id, tag).as_str(),
)
.await
}
pub async fn idm_service_account_unix_extend(
&self,
id: &str,
gidnumber: Option<u32>,
shell: Option<&str>,
) -> Result<(), ClientError> {
let ux = AccountUnixExtend {
shell: shell.map(str::to_string),
gidnumber,
};
self.perform_post_request(format!("/v1/service_account/{}/_unix", id).as_str(), ux)
.await
}
pub async fn idm_service_account_into_person(&self, id: &str) -> Result<(), ClientError> {
self.perform_post_request(
format!("/v1/service_account/{}/_into_person", id).as_str(),
(),
)
.await
}
pub async fn idm_service_account_get_credential_status(
&self,
id: &str,
) -> Result<CredentialStatus, ClientError> {
let res: Result<CredentialStatus, ClientError> = self
.perform_get_request(format!("/v1/service_account/{}/_credential/_status", id).as_str())
.await;
res.and_then(|cs| {
if cs.creds.is_empty() {
Err(ClientError::EmptyResponse)
} else {
Ok(cs)
}
})
}
pub async fn idm_service_account_generate_password(
&self,
id: &str,
) -> Result<String, ClientError> {
let res: Result<String, ClientError> = self
.perform_get_request(
format!("/v1/service_account/{}/_credential/_generate", id).as_str(),
)
.await;
res.and_then(|pw| {
if pw.is_empty() {
Err(ClientError::EmptyResponse)
} else {
Ok(pw)
}
})
}
}

View file

@ -84,7 +84,7 @@ impl From<String> for ConsoleOutputMode {
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum MessageStatus {
Failure,

View file

@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use url::Url;
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Copy)]
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)]
pub enum CodeChallengeMethod {
// default to plain if not requested as S256. Reject the auth?
// plain
@ -173,7 +173,7 @@ impl AccessTokenIntrospectResponse {
}
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ResponseType {
Code,
@ -181,7 +181,7 @@ pub enum ResponseType {
IdToken,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ResponseMode {
Query,
@ -192,7 +192,7 @@ fn response_modes_supported_default() -> Vec<ResponseMode> {
vec![ResponseMode::Query, ResponseMode::Fragment]
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum GrantType {
#[serde(rename = "authorization_code")]
@ -204,14 +204,14 @@ fn grant_types_supported_default() -> Vec<GrantType> {
vec![GrantType::AuthorisationCode, GrantType::Implicit]
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum SubjectType {
Pairwise,
Public,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "UPPERCASE")]
// WE REFUSE TO SUPPORT NONE. DONT EVEN ASK. IT WONT HAPPEN.
pub enum IdTokenSignAlg {
@ -219,7 +219,7 @@ pub enum IdTokenSignAlg {
RS256,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum TokenEndpointAuthMethod {
ClientSecretPost,
@ -232,7 +232,7 @@ fn token_endpoint_auth_methods_supported_default() -> Vec<TokenEndpointAuthMetho
vec![TokenEndpointAuthMethod::ClientSecretBasic]
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DisplayValue {
Page,
@ -241,7 +241,7 @@ pub enum DisplayValue {
Wap,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
// https://openid.net/specs/openid-connect-core-1_0.html#ClaimTypes
pub enum ClaimType {

View file

@ -12,7 +12,7 @@ use webauthn_rs_proto::{
/* ===== errors ===== */
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum SchemaError {
NotImplemented,
@ -22,12 +22,14 @@ pub enum SchemaError {
InvalidAttribute(String),
InvalidAttributeSyntax(String),
AttributeNotValidForClass(String),
SupplementsNotSatisfied(Vec<String>),
ExcludesNotSatisfied(Vec<String>),
EmptyFilter,
Corrupted,
PhantomAttribute(String),
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum PluginError {
AttrUnique(String),
@ -37,7 +39,7 @@ pub enum PluginError {
Oauth2Secrets,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ConsistencyError {
Unknown,
@ -452,12 +454,6 @@ pub struct AccountUnixExtend {
pub shell: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AccountPersonSet {
pub mail: Option<Vec<String>>,
pub legalname: Option<String>,
}
/*
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AccountOrgPersonExtend {
@ -465,7 +461,7 @@ pub struct AccountOrgPersonExtend {
}
*/
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub enum CredentialDetailType {
Password,
GeneratedPassword,
@ -552,7 +548,6 @@ impl fmt::Display for CredentialStatus {
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BackupCodesView {
// Or use SetCredentialResponse::BackupCodes?
pub backup_codes: Vec<String>,
}
@ -563,7 +558,7 @@ pub struct BackupCodesView {
// the in memory server core entry type, without affecting the protoEntry type
//
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
pub struct Entry {
pub attrs: BTreeMap<String, Vec<String>>,
}
@ -841,6 +836,7 @@ pub struct AuthResponse {
}
// Types needed for setting credentials
/*
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SetCredentialRequest {
@ -853,6 +849,7 @@ pub enum SetCredentialRequest {
BackupCodeGenerate,
BackupCodeRemove,
}
*/
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
@ -903,6 +900,7 @@ impl TotpSecret {
}
}
/*
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SetCredentialResponse {
@ -913,13 +911,14 @@ pub enum SetCredentialResponse {
SecurityKeyCreateChallenge(Uuid, CreationChallengeResponse),
BackupCodes(Vec<String>),
}
*/
#[derive(Debug, Serialize, Deserialize)]
pub struct CUIntentToken {
pub token: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CUSessionToken {
pub token: String,
}

View file

@ -27,7 +27,7 @@ impl CommonOpt {
None => client_builder,
};
let ca_path: Option<&str> = self.ca_path.as_ref().map(|p| p.to_str()).flatten();
let ca_path: Option<&str> = self.ca_path.as_ref().and_then(|p| p.to_str());
let client_builder = match ca_path {
Some(p) => {
debug!("Adding trusted CA cert {:?}", p);

View file

@ -17,20 +17,20 @@ use std::path::PathBuf;
include!("../opt/kanidm.rs");
pub mod account;
pub mod common;
pub mod domain;
pub mod group;
pub mod oauth2;
pub mod person;
pub mod raw;
pub mod recycle;
pub mod serviceaccount;
pub mod session;
impl SelfOpt {
pub fn debug(&self) -> bool {
match self {
SelfOpt::Whoami(copt) => copt.debug,
SelfOpt::SetPassword(copt) => copt.debug,
}
}
@ -55,22 +55,6 @@ impl SelfOpt {
Err(e) => println!("Error: {:?}", e),
}
}
SelfOpt::SetPassword(copt) => {
let client = copt.to_client().await;
let password = match rpassword::prompt_password("Enter new password: ") {
Ok(p) => p,
Err(e) => {
error!("Error -> {:?}", e);
return;
}
};
if let Err(e) = client.idm_account_set_password(password).await {
error!("Error -> {:?}", e);
}
}
}
}
}
@ -99,8 +83,9 @@ impl KanidmClientOpt {
KanidmClientOpt::Logout(lopt) => lopt.debug(),
KanidmClientOpt::Session { commands } => commands.debug(),
KanidmClientOpt::CSelf { commands } => commands.debug(),
KanidmClientOpt::Account { commands } => commands.debug(),
KanidmClientOpt::Group { commands } => commands.debug(),
KanidmClientOpt::Person { commands } => commands.debug(),
KanidmClientOpt::ServiceAccount { commands } => commands.debug(),
KanidmClientOpt::System { commands } => commands.debug(),
KanidmClientOpt::Recycle { commands } => commands.debug(),
KanidmClientOpt::Version {} => {
@ -117,7 +102,8 @@ impl KanidmClientOpt {
KanidmClientOpt::Logout(lopt) => lopt.exec().await,
KanidmClientOpt::Session { commands } => commands.exec().await,
KanidmClientOpt::CSelf { commands } => commands.exec().await,
KanidmClientOpt::Account { commands } => commands.exec().await,
KanidmClientOpt::Person { commands } => commands.exec().await,
KanidmClientOpt::ServiceAccount { commands } => commands.exec().await,
KanidmClientOpt::Group { commands } => commands.exec().await,
KanidmClientOpt::System { commands } => commands.exec().await,
KanidmClientOpt::Recycle { commands } => commands.exec().await,

View file

@ -125,7 +125,7 @@ impl Oauth2Opt {
.idm_oauth2_rs_update(
cbopt.nopt.name.as_str(),
None,
Some(&cbopt.displayname.as_str()),
Some(cbopt.displayname.as_str()),
None,
None,
false,

View file

@ -1,14 +1,13 @@
use crate::password_prompt;
use crate::{
AccountCredential, AccountOpt, AccountPerson, AccountPosix, AccountRadius, AccountSsh,
AccountValidity,
AccountCredential, AccountRadius, AccountSsh, AccountValidity, PersonOpt, PersonPosix,
};
use dialoguer::{theme::ColorfulTheme, Select};
use dialoguer::{Confirm, Input, Password};
use kanidm_client::ClientError::Http as ClientErrorHttp;
use kanidm_client::KanidmClient;
use kanidm_proto::messages::{AccountChangeMessage, ConsoleOutputMode, MessageStatus};
use kanidm_proto::v1::OperationError::{InvalidAttribute, PasswordQuality};
use kanidm_proto::v1::OperationError::PasswordQuality;
use kanidm_proto::v1::{CUIntentToken, CURegState, CUSessionToken, CUStatus, TotpSecret};
use qrcode::{render::unicode, QrCode};
use std::fmt::{self, Debug};
@ -19,34 +18,31 @@ use uuid::Uuid;
use webauthn_authenticator_rs::{u2fhid::U2FHid, WebauthnAuthenticator};
impl AccountOpt {
impl PersonOpt {
pub fn debug(&self) -> bool {
match self {
AccountOpt::Credential { commands } => commands.debug(),
AccountOpt::Radius { commands } => match commands {
PersonOpt::Credential { commands } => commands.debug(),
PersonOpt::Radius { commands } => match commands {
AccountRadius::Show(aro) => aro.copt.debug,
AccountRadius::Generate(aro) => aro.copt.debug,
AccountRadius::DeleteSecret(aro) => aro.copt.debug,
},
AccountOpt::Posix { commands } => match commands {
AccountPosix::Show(apo) => apo.copt.debug,
AccountPosix::Set(apo) => apo.copt.debug,
AccountPosix::SetPassword(apo) => apo.copt.debug,
PersonOpt::Posix { commands } => match commands {
PersonPosix::Show(apo) => apo.copt.debug,
PersonPosix::Set(apo) => apo.copt.debug,
PersonPosix::SetPassword(apo) => apo.copt.debug,
},
AccountOpt::Person { commands } => match commands {
AccountPerson::Extend(apo) => apo.copt.debug,
AccountPerson::Set(apo) => apo.copt.debug,
},
AccountOpt::Ssh { commands } => match commands {
PersonOpt::Ssh { commands } => match commands {
AccountSsh::List(ano) => ano.copt.debug,
AccountSsh::Add(ano) => ano.copt.debug,
AccountSsh::Delete(ano) => ano.copt.debug,
},
AccountOpt::List(copt) => copt.debug,
AccountOpt::Get(aopt) => aopt.copt.debug,
AccountOpt::Delete(aopt) => aopt.copt.debug,
AccountOpt::Create(aopt) => aopt.copt.debug,
AccountOpt::Validity { commands } => match commands {
PersonOpt::List(copt) => copt.debug,
PersonOpt::Get(aopt) => aopt.copt.debug,
PersonOpt::Update(aopt) => aopt.copt.debug,
PersonOpt::Delete(aopt) => aopt.copt.debug,
PersonOpt::Create(aopt) => aopt.copt.debug,
PersonOpt::Validity { commands } => match commands {
AccountValidity::Show(ano) => ano.copt.debug,
AccountValidity::ExpireAt(ano) => ano.copt.debug,
AccountValidity::BeginFrom(ano) => ano.copt.debug,
@ -57,8 +53,8 @@ impl AccountOpt {
pub async fn exec(&self) {
match self {
// id/cred/primary/set
AccountOpt::Credential { commands } => commands.exec().await,
AccountOpt::Radius { commands } => match commands {
PersonOpt::Credential { commands } => commands.exec().await,
PersonOpt::Radius { commands } => match commands {
AccountRadius::Show(aopt) => {
let client = aopt.copt.to_client().await;
@ -119,9 +115,9 @@ impl AccountOpt {
}
};
}
}, // end AccountOpt::Radius
AccountOpt::Posix { commands } => match commands {
AccountPosix::Show(aopt) => {
}, // end PersonOpt::Radius
PersonOpt::Posix { commands } => match commands {
PersonPosix::Show(aopt) => {
let client = aopt.copt.to_client().await;
match client
.idm_account_unix_token_get(aopt.aopts.account_id.as_str())
@ -133,10 +129,10 @@ impl AccountOpt {
}
}
}
AccountPosix::Set(aopt) => {
PersonPosix::Set(aopt) => {
let client = aopt.copt.to_client().await;
if let Err(e) = client
.idm_account_unix_extend(
.idm_person_account_unix_extend(
aopt.aopts.account_id.as_str(),
aopt.gidnumber,
aopt.shell.as_deref(),
@ -146,7 +142,7 @@ impl AccountOpt {
error!("Error -> {:?}", e);
}
}
AccountPosix::SetPassword(aopt) => {
PersonPosix::SetPassword(aopt) => {
let client = aopt.copt.to_client().await;
let password = match password_prompt("Enter new posix (sudo) password: ") {
Some(v) => v,
@ -157,7 +153,7 @@ impl AccountOpt {
};
if let Err(e) = client
.idm_account_unix_cred_put(
.idm_person_account_unix_cred_put(
aopt.aopts.account_id.as_str(),
password.as_str(),
)
@ -166,118 +162,8 @@ impl AccountOpt {
error!("Error -> {:?}", e);
}
}
}, // end AccountOpt::Posix
AccountOpt::Person { commands } => match commands {
AccountPerson::Extend(aopt) => {
let client = aopt.copt.to_client().await;
let mut result_output = kanidm_proto::messages::AccountChangeMessage {
output_mode: ConsoleOutputMode::Text,
action: String::from("account_person_extend"),
result: String::from("This is a filler message"),
src_user: aopt
.copt
.username
.to_owned()
.unwrap_or(format!("{:?}", client.whoami().await)),
dest_user: aopt.aopts.account_id.to_string(),
status: kanidm_proto::messages::MessageStatus::Failure,
};
match client
.idm_account_person_extend(
aopt.aopts.account_id.as_str(),
aopt.mail.as_deref(),
aopt.legalname.as_deref(),
)
.await
{
// TODO: JSON output for account person extend.
Ok(_) => {
result_output.status = kanidm_proto::messages::MessageStatus::Success;
result_output.result = format!(
"Successfully extended the user {}.",
aopt.aopts.account_id
);
match &aopt.legalname {
Some(legalname) => {
result_output.result +=
format!(" Set legalname to {}.", legalname).as_str()
}
_ => debug!("Didn't change legalname field."),
};
match &aopt.mail {
Some(mail) => {
result_output.result +=
format!(" Set mail to {:?}.", mail.join(", ")).as_str()
}
_ => debug!("Didn't change mail field."),
};
println!("{}", result_output);
}
// TODO: consider a macro to unpack the KanidmClient result object
Err(e) => {
match e {
ClientErrorHttp(_, result, _) => {
if let Some(ia_error) = result {
match ia_error {
InvalidAttribute(msg) => {
result_output.result =
format!("Failed to set value: {}", msg)
}
_ => {
result_output.result =
format!("Operation Error - {:?}", ia_error)
}
};
} else {
result_output.result =
format!("ClientError - {:?}", result);
}
}
_ => result_output.result = format!("Error -> {:?}", e),
};
eprintln!("{:?}", result_output);
}
}
}
// TODO: there should probably be an 'unset' variant of this
AccountPerson::Set(aopt) => {
let client = aopt.copt.to_client().await;
let mut result_output = AccountChangeMessage {
output_mode: ConsoleOutputMode::Text,
action: String::from("account_person set"),
result: String::from(""),
src_user: aopt
.copt
.username
.to_owned()
.unwrap_or(format!("{:?}", client.whoami().await)),
dest_user: aopt.aopts.account_id.to_string(),
status: MessageStatus::Failure,
};
match client
.idm_account_person_set(
aopt.aopts.account_id.as_str(),
aopt.mail.as_deref(),
aopt.legalname.as_deref(),
)
.await
{
Ok(_) => {
result_output.status = kanidm_proto::messages::MessageStatus::Success;
result_output.result = "set 'person' status on user".to_string();
println!("{}", result_output)
}
// TODO: ponder macro'ing handling ClientError
Err(e) => {
result_output.result = format!("Error -> {:?}", e);
eprintln!("{}", result_output)
}
}
}
}, // end AccountOpt::Person
AccountOpt::Ssh { commands } => match commands {
}, // end PersonOpt::Posix
PersonOpt::Ssh { commands } => match commands {
AccountSsh::List(aopt) => {
let client = aopt.copt.to_client().await;
@ -294,7 +180,7 @@ impl AccountOpt {
AccountSsh::Add(aopt) => {
let client = aopt.copt.to_client().await;
if let Err(e) = client
.idm_account_post_ssh_pubkey(
.idm_person_account_post_ssh_pubkey(
aopt.aopts.account_id.as_str(),
aopt.tag.as_str(),
aopt.pubkey.as_str(),
@ -307,7 +193,7 @@ impl AccountOpt {
AccountSsh::Delete(aopt) => {
let client = aopt.copt.to_client().await;
if let Err(e) = client
.idm_account_delete_ssh_pubkey(
.idm_person_account_delete_ssh_pubkey(
aopt.aopts.account_id.as_str(),
aopt.tag.as_str(),
)
@ -316,23 +202,42 @@ impl AccountOpt {
error!("Error -> {:?}", e);
}
}
}, // end AccountOpt::Ssh
AccountOpt::List(copt) => {
}, // end PersonOpt::Ssh
PersonOpt::List(copt) => {
let client = copt.to_client().await;
match client.idm_account_list().await {
match client.idm_person_account_list().await {
Ok(r) => r.iter().for_each(|ent| println!("{}", ent)),
Err(e) => error!("Error -> {:?}", e),
}
}
AccountOpt::Get(aopt) => {
PersonOpt::Update(aopt) => {
let client = aopt.copt.to_client().await;
match client.idm_account_get(aopt.aopts.account_id.as_str()).await {
match client
.idm_person_account_update(
aopt.aopts.account_id.as_str(),
aopt.newname.as_deref(),
aopt.displayname.as_deref(),
aopt.legalname.as_deref(),
aopt.mail.as_deref(),
)
.await
{
Ok(()) => println!("Success"),
Err(e) => error!("Error -> {:?}", e),
}
}
PersonOpt::Get(aopt) => {
let client = aopt.copt.to_client().await;
match client
.idm_person_account_get(aopt.aopts.account_id.as_str())
.await
{
Ok(Some(e)) => println!("{}", e),
Ok(None) => println!("No matching entries"),
Err(e) => error!("Error -> {:?}", e),
}
}
AccountOpt::Delete(aopt) => {
PersonOpt::Delete(aopt) => {
let client = aopt.copt.to_client().await;
let mut modmessage = AccountChangeMessage {
output_mode: ConsoleOutputMode::Text,
@ -347,7 +252,7 @@ impl AccountOpt {
status: MessageStatus::Success,
};
match client
.idm_account_delete(aopt.aopts.account_id.as_str())
.idm_person_account_delete(aopt.aopts.account_id.as_str())
.await
{
Err(e) => {
@ -361,10 +266,10 @@ impl AccountOpt {
}
};
}
AccountOpt::Create(acopt) => {
PersonOpt::Create(acopt) => {
let client = acopt.copt.to_client().await;
if let Err(e) = client
.idm_account_create(
.idm_person_account_create(
acopt.aopts.account_id.as_str(),
acopt.display_name.as_str(),
)
@ -373,13 +278,16 @@ impl AccountOpt {
error!("Error -> {:?}", e)
}
}
AccountOpt::Validity { commands } => match commands {
PersonOpt::Validity { commands } => match commands {
AccountValidity::Show(ano) => {
let client = ano.copt.to_client().await;
println!("user: {}", ano.aopts.account_id.as_str());
let ex = match client
.idm_account_get_attr(ano.aopts.account_id.as_str(), "account_expire")
.idm_person_account_get_attr(
ano.aopts.account_id.as_str(),
"account_expire",
)
.await
{
Ok(v) => v,
@ -390,7 +298,10 @@ impl AccountOpt {
};
let vf = match client
.idm_account_get_attr(ano.aopts.account_id.as_str(), "account_valid_from")
.idm_person_account_get_attr(
ano.aopts.account_id.as_str(),
"account_valid_from",
)
.await
{
Ok(v) => v,
@ -437,7 +348,10 @@ impl AccountOpt {
if matches!(ano.datetime.as_str(), "never" | "clear") {
// Unset the value
match client
.idm_account_purge_attr(ano.aopts.account_id.as_str(), "account_expire")
.idm_person_account_purge_attr(
ano.aopts.account_id.as_str(),
"account_expire",
)
.await
{
Err(e) => error!("Error -> {:?}", e),
@ -452,7 +366,7 @@ impl AccountOpt {
}
match client
.idm_account_set_attr(
.idm_person_account_set_attr(
ano.aopts.account_id.as_str(),
"account_expire",
&[ano.datetime.as_str()],
@ -469,7 +383,7 @@ impl AccountOpt {
if matches!(ano.datetime.as_str(), "any" | "clear" | "whenever") {
// Unset the value
match client
.idm_account_purge_attr(
.idm_person_account_purge_attr(
ano.aopts.account_id.as_str(),
"account_valid_from",
)
@ -488,7 +402,7 @@ impl AccountOpt {
}
match client
.idm_account_set_attr(
.idm_person_account_set_attr(
ano.aopts.account_id.as_str(),
"account_valid_from",
&[ano.datetime.as_str()],
@ -500,7 +414,7 @@ impl AccountOpt {
}
}
}
}, // end AccountOpt::Validity
}, // end PersonOpt::Validity
}
}
}
@ -508,6 +422,7 @@ impl AccountOpt {
impl AccountCredential {
pub fn debug(&self) -> bool {
match self {
AccountCredential::Status(aopt) => aopt.copt.debug,
AccountCredential::CreateResetToken(aopt) => aopt.copt.debug,
AccountCredential::UseResetToken(aopt) => aopt.copt.debug,
AccountCredential::Update(aopt) => aopt.copt.debug,
@ -516,6 +431,20 @@ impl AccountCredential {
pub async fn exec(&self) {
match self {
AccountCredential::Status(aopt) => {
let client = aopt.copt.to_client().await;
match client
.idm_person_account_get_credential_status(aopt.aopts.account_id.as_str())
.await
{
Ok(cstatus) => {
println!("{}", cstatus);
}
Err(e) => {
error!("Error getting credential status -> {:?}", e);
}
}
}
AccountCredential::Update(aopt) => {
let client = aopt.copt.to_client().await;
match client
@ -563,7 +492,7 @@ impl AccountCredential {
// What's the client url?
match client
.idm_account_credential_update_intent(aopt.aopts.account_id.as_str())
.idm_person_account_credential_update_intent(aopt.aopts.account_id.as_str())
.await
{
Ok(cuintent_token) => {

View file

@ -0,0 +1,359 @@
use crate::{
AccountSsh, AccountValidity, ServiceAccountCredential, ServiceAccountOpt, ServiceAccountPosix,
};
use kanidm_proto::messages::{AccountChangeMessage, ConsoleOutputMode, MessageStatus};
use time::OffsetDateTime;
impl ServiceAccountOpt {
pub fn debug(&self) -> bool {
match self {
ServiceAccountOpt::Credential { commands } => match commands {
ServiceAccountCredential::Status(apo) => apo.copt.debug,
ServiceAccountCredential::GeneratePw(apo) => apo.copt.debug,
},
ServiceAccountOpt::Posix { commands } => match commands {
ServiceAccountPosix::Show(apo) => apo.copt.debug,
ServiceAccountPosix::Set(apo) => apo.copt.debug,
},
ServiceAccountOpt::Ssh { commands } => match commands {
AccountSsh::List(ano) => ano.copt.debug,
AccountSsh::Add(ano) => ano.copt.debug,
AccountSsh::Delete(ano) => ano.copt.debug,
},
ServiceAccountOpt::List(copt) => copt.debug,
ServiceAccountOpt::Get(aopt) => aopt.copt.debug,
ServiceAccountOpt::Update(aopt) => aopt.copt.debug,
ServiceAccountOpt::Delete(aopt) => aopt.copt.debug,
ServiceAccountOpt::Create(aopt) => aopt.copt.debug,
ServiceAccountOpt::Validity { commands } => match commands {
AccountValidity::Show(ano) => ano.copt.debug,
AccountValidity::ExpireAt(ano) => ano.copt.debug,
AccountValidity::BeginFrom(ano) => ano.copt.debug,
},
ServiceAccountOpt::IntoPerson(aopt) => aopt.copt.debug,
}
}
pub async fn exec(&self) {
match self {
ServiceAccountOpt::Credential { commands } => match commands {
ServiceAccountCredential::Status(apo) => {
let client = apo.copt.to_client().await;
match client
.idm_service_account_get_credential_status(apo.aopts.account_id.as_str())
.await
{
Ok(cstatus) => {
println!("{}", cstatus);
}
Err(e) => {
error!("Error getting credential status -> {:?}", e);
}
}
}
ServiceAccountCredential::GeneratePw(apo) => {
let client = apo.copt.to_client().await;
match client
.idm_service_account_generate_password(apo.aopts.account_id.as_str())
.await
{
Ok(new_pw) => {
println!("Success: {}", new_pw);
}
Err(e) => {
error!("Error generating service account credential-> {:?}", e);
}
}
}
},
ServiceAccountOpt::Posix { commands } => match commands {
ServiceAccountPosix::Show(aopt) => {
let client = aopt.copt.to_client().await;
match client
.idm_account_unix_token_get(aopt.aopts.account_id.as_str())
.await
{
Ok(token) => println!("{}", token),
Err(e) => {
error!("Error -> {:?}", e);
}
}
}
ServiceAccountPosix::Set(aopt) => {
let client = aopt.copt.to_client().await;
if let Err(e) = client
.idm_service_account_unix_extend(
aopt.aopts.account_id.as_str(),
aopt.gidnumber,
aopt.shell.as_deref(),
)
.await
{
error!("Error -> {:?}", e);
}
}
}, // end ServiceAccountOpt::Posix
ServiceAccountOpt::Ssh { commands } => match commands {
AccountSsh::List(aopt) => {
let client = aopt.copt.to_client().await;
match client
.idm_account_get_ssh_pubkeys(aopt.aopts.account_id.as_str())
.await
{
Ok(pkeys) => pkeys.iter().for_each(|pkey| println!("{}", pkey)),
Err(e) => {
error!("Error -> {:?}", e);
}
}
}
AccountSsh::Add(aopt) => {
let client = aopt.copt.to_client().await;
if let Err(e) = client
.idm_service_account_post_ssh_pubkey(
aopt.aopts.account_id.as_str(),
aopt.tag.as_str(),
aopt.pubkey.as_str(),
)
.await
{
error!("Error -> {:?}", e);
}
}
AccountSsh::Delete(aopt) => {
let client = aopt.copt.to_client().await;
if let Err(e) = client
.idm_service_account_delete_ssh_pubkey(
aopt.aopts.account_id.as_str(),
aopt.tag.as_str(),
)
.await
{
error!("Error -> {:?}", e);
}
}
}, // end ServiceAccountOpt::Ssh
ServiceAccountOpt::List(copt) => {
let client = copt.to_client().await;
match client.idm_service_account_list().await {
Ok(r) => r.iter().for_each(|ent| println!("{}", ent)),
Err(e) => error!("Error -> {:?}", e),
}
}
ServiceAccountOpt::Update(aopt) => {
let client = aopt.copt.to_client().await;
match client
.idm_service_account_update(
aopt.aopts.account_id.as_str(),
aopt.newname.as_deref(),
aopt.displayname.as_deref(),
aopt.mail.as_deref(),
)
.await
{
Ok(()) => println!("Success"),
Err(e) => error!("Error -> {:?}", e),
}
}
ServiceAccountOpt::Get(aopt) => {
let client = aopt.copt.to_client().await;
match client
.idm_service_account_get(aopt.aopts.account_id.as_str())
.await
{
Ok(Some(e)) => println!("{}", e),
Ok(None) => println!("No matching entries"),
Err(e) => error!("Error -> {:?}", e),
}
}
ServiceAccountOpt::Delete(aopt) => {
let client = aopt.copt.to_client().await;
let mut modmessage = AccountChangeMessage {
output_mode: ConsoleOutputMode::Text,
action: "account delete".to_string(),
result: "deleted".to_string(),
src_user: aopt
.copt
.username
.to_owned()
.unwrap_or(format!("{:?}", client.whoami().await)),
dest_user: aopt.aopts.account_id.to_string(),
status: MessageStatus::Success,
};
match client
.idm_service_account_delete(aopt.aopts.account_id.as_str())
.await
{
Err(e) => {
modmessage.result = format!("Error -> {:?}", e);
modmessage.status = MessageStatus::Failure;
eprintln!("{}", modmessage);
}
Ok(result) => {
debug!("{:?}", result);
println!("{}", modmessage);
}
};
}
ServiceAccountOpt::Create(acopt) => {
let client = acopt.copt.to_client().await;
if let Err(e) = client
.idm_service_account_create(
acopt.aopts.account_id.as_str(),
acopt.display_name.as_str(),
)
.await
{
error!("Error -> {:?}", e)
}
}
ServiceAccountOpt::Validity { commands } => match commands {
AccountValidity::Show(ano) => {
let client = ano.copt.to_client().await;
println!("user: {}", ano.aopts.account_id.as_str());
let ex = match client
.idm_service_account_get_attr(
ano.aopts.account_id.as_str(),
"account_expire",
)
.await
{
Ok(v) => v,
Err(e) => {
error!("Error -> {:?}", e);
return;
}
};
let vf = match client
.idm_service_account_get_attr(
ano.aopts.account_id.as_str(),
"account_valid_from",
)
.await
{
Ok(v) => v,
Err(e) => {
error!("Error -> {:?}", e);
return;
}
};
if let Some(t) = vf {
// Convert the time to local timezone.
let t = OffsetDateTime::parse(&t[0], time::Format::Rfc3339)
.map(|odt| {
odt.to_offset(
time::UtcOffset::try_current_local_offset()
.unwrap_or(time::UtcOffset::UTC),
)
.format(time::Format::Rfc3339)
})
.unwrap_or_else(|_| "invalid timestamp".to_string());
println!("valid after: {}", t);
} else {
println!("valid after: any time");
}
if let Some(t) = ex {
let t = OffsetDateTime::parse(&t[0], time::Format::Rfc3339)
.map(|odt| {
odt.to_offset(
time::UtcOffset::try_current_local_offset()
.unwrap_or(time::UtcOffset::UTC),
)
.format(time::Format::Rfc3339)
})
.unwrap_or_else(|_| "invalid timestamp".to_string());
println!("expire: {}", t);
} else {
println!("expire: never");
}
}
AccountValidity::ExpireAt(ano) => {
let client = ano.copt.to_client().await;
if matches!(ano.datetime.as_str(), "never" | "clear") {
// Unset the value
match client
.idm_service_account_purge_attr(
ano.aopts.account_id.as_str(),
"account_expire",
)
.await
{
Err(e) => error!("Error -> {:?}", e),
_ => println!("Success"),
}
} else {
if let Err(e) =
OffsetDateTime::parse(ano.datetime.as_str(), time::Format::Rfc3339)
{
error!("Error -> {:?}", e);
return;
}
match client
.idm_service_account_set_attr(
ano.aopts.account_id.as_str(),
"account_expire",
&[ano.datetime.as_str()],
)
.await
{
Err(e) => error!("Error -> {:?}", e),
_ => println!("Success"),
}
}
}
AccountValidity::BeginFrom(ano) => {
let client = ano.copt.to_client().await;
if matches!(ano.datetime.as_str(), "any" | "clear" | "whenever") {
// Unset the value
match client
.idm_service_account_purge_attr(
ano.aopts.account_id.as_str(),
"account_valid_from",
)
.await
{
Err(e) => error!("Error -> {:?}", e),
_ => println!("Success"),
}
} else {
// Attempt to parse and set
if let Err(e) =
OffsetDateTime::parse(ano.datetime.as_str(), time::Format::Rfc3339)
{
error!("Error -> {:?}", e);
return;
}
match client
.idm_service_account_set_attr(
ano.aopts.account_id.as_str(),
"account_valid_from",
&[ano.datetime.as_str()],
)
.await
{
Err(e) => error!("Error -> {:?}", e),
_ => println!("Success"),
}
}
}
}, // end ServiceAccountOpt::Validity
ServiceAccountOpt::IntoPerson(aopt) => {
let client = aopt.copt.to_client().await;
match client
.idm_service_account_into_person(aopt.aopts.account_id.as_str())
.await
{
Ok(()) => println!("Success"),
Err(e) => error!("Error -> {:?}", e),
}
}
}
}
}

View file

@ -9,15 +9,17 @@ pub struct Named {
#[derive(Debug, Args)]
pub struct DebugOpt {
/// Enable debbuging of the kanidm tool
#[clap(short, long, env = "KANIDM_DEBUG")]
pub debug: bool,
}
#[derive(Debug, Args)]
pub struct CommonOpt {
// TODO: this should probably be a flag, or renamed to log level if it's a level
/// Enable debbuging of the kanidm tool
#[clap(short, long, env = "KANIDM_DEBUG")]
pub debug: bool,
/// The URL of the kanidm instance
#[clap(short = 'H', long = "url", env = "KANIDM_URL")]
pub addr: Option<String>,
/// User which will initiate requests
@ -47,32 +49,45 @@ pub struct GroupPosixOpt {
#[derive(Debug, Subcommand)]
pub enum GroupPosix {
/// Show details of a specific posix group
#[clap(name = "show")]
Show(Named),
/// Setup posix group properties, or alter them
#[clap(name = "set")]
Set(GroupPosixOpt),
}
#[derive(Debug, Subcommand)]
pub enum GroupOpt {
/// List all groups
#[clap(name = "list")]
List(CommonOpt),
/// View a specific group
#[clap(name = "get")]
Get(Named),
/// Create a new group
#[clap(name = "create")]
Create(Named),
/// Delete a group
#[clap(name = "delete")]
Delete(Named),
/// List the members of a group
#[clap(name = "list_members")]
ListMembers(Named),
/// Set the exact list of members that this group should contain, removing any not listed in the
/// set operation.
#[clap(name = "set_members")]
SetMembers(GroupNamedMembers),
/// Delete all members of a group.
#[clap(name = "purge_members")]
PurgeMembers(Named),
/// Add new members to a group
#[clap(name = "add_members")]
AddMembers(GroupNamedMembers),
/// Remove the named members from this group
#[clap(name = "remove_members")]
RemoveMembers(GroupNamedMembers),
/// Manage posix extensions for this group allowing groups to be used on unix/linux systems
#[clap(name = "posix")]
Posix {
#[clap(subcommand)]
@ -161,6 +176,9 @@ pub struct AccountCreateOpt {
#[derive(Debug, Subcommand)]
pub enum AccountCredential {
/// Show the status of this accounts credentials.
#[clap(name = "status")]
Status(AccountNamedOpt),
/// Interactively update/change the credentials for an account
#[clap(name = "update")]
Update(AccountNamedOpt),
@ -200,7 +218,7 @@ pub struct AccountPosixOpt {
}
#[derive(Debug, Subcommand)]
pub enum AccountPosix {
pub enum PersonPosix {
#[clap(name = "show")]
Show(AccountNamedOpt),
#[clap(name = "set")]
@ -209,24 +227,28 @@ pub enum AccountPosix {
SetPassword(AccountNamedOpt),
}
#[derive(Debug, Args)]
pub struct AccountPersonOpt {
#[clap(flatten)]
aopts: AccountCommonOpt,
#[clap(long, short, help="Set the mail address, can be set multiple times for multiple addresses.")]
mail: Option<Vec<String>>,
#[clap(long, short, help="Set the legal name for the person.")]
legalname: Option<String>,
#[clap(flatten)]
copt: CommonOpt,
#[derive(Debug, Subcommand)]
pub enum ServiceAccountPosix {
#[clap(name = "show")]
Show(AccountNamedOpt),
#[clap(name = "set")]
Set(AccountPosixOpt),
}
#[derive(Debug, Subcommand)]
pub enum AccountPerson {
#[clap(name = "extend")]
Extend(AccountPersonOpt),
#[clap(name = "set")]
Set(AccountPersonOpt),
#[derive(Debug, Args)]
pub struct PersonUpdateOpt {
#[clap(flatten)]
aopts: AccountCommonOpt,
#[clap(long, short, help="Set the legal name for the person.")]
legalname: Option<String>,
#[clap(long, short, help="Set the account name for the person.")]
newname: Option<String>,
#[clap(long, short='i', help="Set the display name for the person.")]
displayname: Option<String>,
#[clap(long, short, help="Set the mail address, can be set multiple times for multiple addresses. The first listed mail address is the 'primary'")]
mail: Option<Vec<String>>,
#[clap(flatten)]
copt: CommonOpt,
}
#[derive(Debug, Subcommand)]
@ -241,49 +263,59 @@ pub enum AccountSsh {
#[derive(Debug, Subcommand)]
pub enum AccountValidity {
/// Show an accounts validity window
#[clap(name = "show")]
Show(AccountNamedOpt),
/// Set an accounts expiry time
#[clap(name = "expire_at")]
ExpireAt(AccountNamedExpireDateTimeOpt),
/// Set an account valid from time
#[clap(name = "begin_from")]
BeginFrom(AccountNamedValidDateTimeOpt),
}
#[derive(Debug, Subcommand)]
pub enum AccountOpt {
pub enum PersonOpt {
/// Manage the credentials this person uses for authentication
#[clap(name = "credential")]
Credential {
#[clap(subcommand)]
commands: AccountCredential,
},
/// Manage radius access for this person
#[clap(name = "radius")]
Radius {
#[clap(subcommand)]
commands: AccountRadius,
},
/// Manage posix extensions for this person allowing access to unix/linux systems
#[clap(name = "posix")]
Posix {
#[clap(subcommand)]
commands: AccountPosix,
},
#[clap(name = "person")]
Person {
#[clap(subcommand)]
commands: AccountPerson,
commands: PersonPosix,
},
/// Manage ssh public key's associated to this person
#[clap(name = "ssh")]
Ssh {
#[clap(subcommand)]
commands: AccountSsh,
},
/// List all persons
#[clap(name = "list")]
List(CommonOpt),
/// View a specific person
#[clap(name = "get")]
Get(AccountNamedOpt),
/// Update a specific person's attributes
#[clap(name = "update")]
Update(PersonUpdateOpt),
/// Create a new person's account
#[clap(name = "create")]
Create(AccountCreateOpt),
/// Delete a person's account
#[clap(name = "delete")]
Delete(AccountNamedOpt),
/// Manage a person's account validity, such as expiry time (account lock/unlock)
#[clap(name = "validity")]
Validity {
#[clap(subcommand)]
@ -291,6 +323,80 @@ pub enum AccountOpt {
},
}
#[derive(Debug, Subcommand)]
pub enum ServiceAccountCredential {
/// Show the status of this accounts credentials.
#[clap(name = "status")]
Status(AccountNamedOpt),
/// Reset and generate a new service account password. This password can NOT
/// be used with the LDAP interface.
#[clap(name = "generate-pw")]
GeneratePw(AccountNamedOpt),
// Future - add a token creator / remover.
}
#[derive(Debug, Args)]
pub struct ServiceAccountUpdateOpt {
#[clap(flatten)]
aopts: AccountCommonOpt,
#[clap(long, short, help="Set the account name for the service account.")]
newname: Option<String>,
#[clap(long, short='i', help="Set the display name for the service account.")]
displayname: Option<String>,
#[clap(long, short, help="Set the mail address, can be set multiple times for multiple addresses. The first listed mail address is the 'primary'")]
mail: Option<Vec<String>>,
#[clap(flatten)]
copt: CommonOpt,
}
#[derive(Debug, Subcommand)]
pub enum ServiceAccountOpt {
/// Manage generated passwords or access tokens for this service account.
#[clap(name = "credential")]
Credential {
#[clap(subcommand)]
commands: ServiceAccountCredential,
},
/// Manage posix extensions for this service account allowing access to unix/linux systems
#[clap(name = "posix")]
Posix {
#[clap(subcommand)]
commands: ServiceAccountPosix,
},
/// Manage ssh public key's associated to this person
#[clap(name = "ssh")]
Ssh {
#[clap(subcommand)]
commands: AccountSsh,
},
/// List all service accounts
#[clap(name = "list")]
List(CommonOpt),
/// View a specific service account
#[clap(name = "get")]
Get(AccountNamedOpt),
/// Create a new service account
#[clap(name = "create")]
Create(AccountCreateOpt),
/// Update a specific service account's attributes
#[clap(name = "update")]
Update(ServiceAccountUpdateOpt),
/// Delete a service account
#[clap(name = "delete")]
Delete(AccountNamedOpt),
/// Manage a service account validity, such as expiry time (account lock/unlock)
#[clap(name = "validity")]
Validity {
#[clap(subcommand)]
commands: AccountValidity,
},
/// Convert a service account into a person. This is used during the alpha.9
/// to alpha.10 migration to "fix up" accounts that were not previously marked
/// as persons.
#[clap(name = "into-person")]
IntoPerson(AccountNamedOpt),
}
#[derive(Debug, Subcommand)]
pub enum RecycleOpt {
#[clap(name = "list")]
@ -376,9 +482,6 @@ pub enum RawOpt {
pub enum SelfOpt {
/// Show the current authenticated user's identity
Whoami(CommonOpt),
#[clap(name = "set_password")]
/// Set the current user's password
SetPassword(CommonOpt),
}
#[derive(Debug, Args)]
@ -530,28 +633,34 @@ pub enum KanidmClientOpt {
#[clap(subcommand)]
commands: SelfOpt,
},
/// Account operations
Account {
/// Actions to manage and view person (user) accounts
Person {
#[clap(subcommand)]
commands: AccountOpt,
commands: PersonOpt
},
/// Group operations
/// Actions to manage groups
Group {
#[clap(subcommand)]
commands: GroupOpt,
},
/// Actions to manage and view service accounts
#[clap(name = "service-account")]
ServiceAccount {
#[clap(subcommand)]
commands: ServiceAccountOpt,
},
/// System configuration operations
System {
#[clap(subcommand)]
commands: SystemOpt,
},
#[clap(name = "recycle_bin")]
#[clap(name = "recycle-bin")]
/// Recycle Bin operations
Recycle {
#[clap(subcommand)]
commands: RecycleOpt,
},
/// Unsafe - low level, raw database operations.
/// Unsafe - low level, raw database queries and operations.
Raw {
#[clap(subcommand)]
commands: RawOpt,

View file

@ -139,24 +139,24 @@ async fn test_fixture(rsclient: KanidmClient) {
// Create a new account
rsclient
.idm_account_create("testaccount1", "Posix Demo Account")
.idm_person_account_create("testaccount1", "Posix Demo Account")
.await
.unwrap();
// Extend the account with posix attrs.
rsclient
.idm_account_unix_extend("testaccount1", Some(20000), None)
.idm_person_account_unix_extend("testaccount1", Some(20000), None)
.await
.unwrap();
// Assign an ssh public key.
rsclient
.idm_account_post_ssh_pubkey("testaccount1", "tk",
.idm_person_account_post_ssh_pubkey("testaccount1", "tk",
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAeGW1P6Pc2rPq0XqbRaDKBcXZUPRklo0L1EyR30CwoP william@amethyst")
.await
.unwrap();
// Set a posix password
rsclient
.idm_account_unix_cred_put("testaccount1", TESTACCOUNT1_PASSWORD_A)
.idm_person_account_unix_cred_put("testaccount1", TESTACCOUNT1_PASSWORD_A)
.await
.unwrap();
@ -370,7 +370,7 @@ async fn test_cache_account_delete() {
.await
.expect("failed to auth as admin");
adminclient
.idm_account_delete("testaccount1")
.idm_person_account_delete("testaccount1")
.await
.expect("failed to delete");
@ -420,7 +420,7 @@ async fn test_cache_account_password() {
.await
.expect("failed to auth as admin");
adminclient
.idm_account_unix_cred_put("testaccount1", TESTACCOUNT1_PASSWORD_B)
.idm_person_account_unix_cred_put("testaccount1", TESTACCOUNT1_PASSWORD_B)
.await
.expect("Failed to change password");
@ -570,7 +570,7 @@ async fn test_cache_account_expiry() {
.await
.expect("failed to auth as admin");
adminclient
.idm_account_set_attr("testaccount1", "account_expire", &[ACCOUNT_EXPIRE])
.idm_person_account_set_attr("testaccount1", "account_expire", &[ACCOUNT_EXPIRE])
.await
.unwrap();
// auth will fail

View file

@ -2,6 +2,9 @@
# This script based on the developer readme and allows you to run a test server.
if [ -z "$KANI_TMP" ]; then
KANI_TMP=/tmp/kanidm
fi
CONFIG_FILE="../../examples/insecure_server.toml"
@ -10,11 +13,11 @@ if [ ! -f "${CONFIG_FILE}" ]; then
echo "Couldn't find configuration file at ${CONFIG_FILE}, please ensure you're running this script from its base directory (${SCRIPT_DIR})."
exit 1
fi
if [ ! -f "/tmp/kanidm/chain.pem" ]; then
if [ ! -f "${KANI_TMP}/chain.pem" ]; then
echo "Couldn't find certificate at /tmp/kanidm/chain.pem, quitting"
exit 1
fi
if [ ! -f "/tmp/kanidm/key.pem" ]; then
if [ ! -f "${KANI_TMP}/key.pem" ]; then
echo "Couldn't find key file at /tmp/kanidm/key.pem, quitting"
exit 1
fi

View file

@ -367,7 +367,7 @@ impl AccessControlProfile {
}
}
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccessEffectivePermission {
// I don't think we need this? The ident is implied by the requestor.
// ident: Uuid,
@ -804,7 +804,7 @@ pub trait AccessControlsTransaction<'a> {
// Find the acps that relate to the caller, and compile their related
// target filters.
let related_acp: Vec<(&AccessControlModify, _)> =
self.modify_related_acp(&rec_entry, &me.ident);
self.modify_related_acp(rec_entry, &me.ident);
related_acp.iter().for_each(|racp| {
trace!("Related acs -> {:?}", racp.0.acp.name);
@ -1277,7 +1277,7 @@ pub trait AccessControlsTransaction<'a> {
let modify_class: BTreeSet<AttrString> = modify_scoped_acp
.iter()
.flat_map(|acp| acp.classes.iter().map(|v| v.clone()))
.flat_map(|acp| acp.classes.iter().cloned())
.collect();
AccessEffectivePermission {

View file

@ -181,7 +181,7 @@ impl QueryServerReadV1 {
msg: OnlineBackupEvent,
outpath: &str,
versions: usize,
) {
) -> Result<(), OperationError> {
trace!(eventid = ?msg.eventid, "Begin online backup event");
let now: DateTime<Utc> = Utc::now();
@ -194,46 +194,50 @@ impl QueryServerReadV1 {
"Online backup file {} already exists, will not owerwrite it.",
dest_file
);
return Err(OperationError::InvalidState);
}
false => {
let idms_prox_read = self.idms.proxy_read_async().await;
spanned!("actors::v1_read::handle<OnlineBackupEvent>", {
let res = idms_prox_read.qs_read.get_be_txn().backup(&dest_file);
match &res {
Ok(()) => {
idms_prox_read
.qs_read
.get_be_txn()
.backup(&dest_file)
.map(|()| {
info!("Online backup created {} successfully", dest_file);
}
Err(e) => {
})
.map_err(|e| {
error!("Online backup failed to create {}: {:?}", dest_file, e);
}
}
admin_info!(?res, "online backup result");
OperationError::InvalidState
})?;
});
}
}
// cleanup of maximum backup versions to keep
let mut backup_file_list: Vec<PathBuf> = Vec::new();
// pattern to find automatically generated backup files
let re = match Regex::new(r"^backup-\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z\.json$") {
Ok(value) => value,
Err(error) => {
eprintln!(
let re = Regex::new(r"^backup-\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z\.json$").map_err(
|error| {
error!(
"Failed to parse regexp for online backup files: {:?}",
error
);
return;
}
};
OperationError::InvalidState
},
)?;
// cleanup of maximum backup versions to keep
let mut backup_file_list: Vec<PathBuf> = Vec::new();
// get a list of backup files
match fs::read_dir(outpath) {
Ok(rd) => {
for entry in rd {
// get PathBuf
let pb = entry.unwrap().path();
let pb = entry
.map_err(|e| {
error!(?e, "Pathbuf access");
OperationError::InvalidState
})?
.path();
// skip everything that is not a file
if !pb.is_file() {
@ -241,7 +245,10 @@ impl QueryServerReadV1 {
}
// get the /some/dir/<file_name> of the file
let file_name = pb.file_name().unwrap().to_str().unwrap();
let file_name = pb.file_name().and_then(|f| f.to_str()).ok_or_else(|| {
error!("filename is invalid");
OperationError::InvalidState
})?;
// check for a online backup file
if re.is_match(file_name) {
backup_file_list.push(pb.clone());
@ -250,6 +257,7 @@ impl QueryServerReadV1 {
}
Err(e) => {
error!("Online backup cleanup error read dir {}: {}", outpath, e);
return Err(OperationError::InvalidState);
}
}
@ -287,6 +295,8 @@ impl QueryServerReadV1 {
} else {
debug!("Online backup cleanup had no files to remove");
};
Ok(())
}
#[instrument(
@ -1113,7 +1123,7 @@ impl QueryServerReadV1 {
e
}),
CURequest::PasskeyFinish(label, rpkc) => idms_cred_update
.credential_passkey_finish(&session_token, ct, label, rpkc)
.credential_passkey_finish(&session_token, ct, label, &rpkc)
.map_err(|e| {
admin_error!(
err = ?e,

View file

@ -3,8 +3,6 @@ use std::sync::Arc;
use std::time::Duration;
use tracing::{info, instrument, span, trace, Level};
use crate::idm::event::GenerateBackupCodeEvent;
use crate::idm::event::RemoveBackupCodeEvent;
use crate::prelude::*;
use crate::idm::credupdatesession::{
@ -17,8 +15,7 @@ use crate::event::{
ReviveRecycledEvent,
};
use crate::idm::event::{
AcceptSha1TotpEvent, GeneratePasswordEvent, GenerateTotpEvent, PasswordChangeEvent,
RegenerateRadiusSecretEvent, RemoveTotpEvent, UnixPasswordChangeEvent, VerifyTotpEvent,
GeneratePasswordEvent, RegenerateRadiusSecretEvent, UnixPasswordChangeEvent,
};
use crate::modify::{Modify, ModifyInvalid, ModifyList};
use crate::value::{PartialValue, Value};
@ -33,8 +30,8 @@ use kanidm_proto::v1::Entry as ProtoEntry;
use kanidm_proto::v1::Modify as ProtoModify;
use kanidm_proto::v1::ModifyList as ProtoModifyList;
use kanidm_proto::v1::{
AccountPersonSet, AccountUnixExtend, CUIntentToken, CUSessionToken, CUStatus, CreateRequest,
DeleteRequest, GroupUnixExtend, ModifyRequest, SetCredentialRequest, SetCredentialResponse,
AccountUnixExtend, CUIntentToken, CUSessionToken, CUStatus, CreateRequest, DeleteRequest,
GroupUnixExtend, ModifyRequest,
};
use uuid::Uuid;
@ -424,28 +421,21 @@ impl QueryServerWriteV1 {
res
}
// === IDM native types for modifications
#[instrument(
level = "info",
name = "credential_set",
skip(self, uat, uuid_or_name, sac, eventid)
name = "service_account_credential_generate",
skip(self, uat, uuid_or_name, eventid)
fields(uuid = ?eventid)
)]
pub async fn handle_credentialset(
pub async fn handle_service_account_credential_generate(
&self,
uat: Option<String>,
uuid_or_name: String,
sac: SetCredentialRequest,
eventid: Uuid,
) -> Result<SetCredentialResponse, OperationError> {
) -> Result<String, OperationError> {
let ct = duration_from_epoch_now();
let mut idms_prox_write = self.idms.proxy_write_async(ct).await;
let res = spanned!("actors::v1_write::handle<InternalCredentialSetMessage>", {
// Trigger a session clean *before* we take any auth steps.
// It's important to do this before to ensure that timeouts on
// the session are enforced.
idms_prox_write.expire_mfareg_sessions(ct);
let ident = idms_prox_write
.validate_and_parse_uat(uat.as_deref(), ct)
.and_then(|uat| idms_prox_write.process_uat_to_identity(&uat, ct))
@ -467,204 +457,16 @@ impl QueryServerWriteV1 {
e
})?;
// What type of auth set did we recieve?
match sac {
SetCredentialRequest::Password(cleartext) => {
let pce = PasswordChangeEvent::from_parts(
// &idms_prox_write.qs_write,
ident,
target_uuid,
cleartext,
)
.map_err(|e| {
admin_error!(
err = ?e,
"Failed to begin internal_credential_set_message",
);
e
})?;
idms_prox_write
.set_account_password(&pce)
.and_then(|_| idms_prox_write.commit())
.map(|_| SetCredentialResponse::Success)
}
SetCredentialRequest::GeneratePassword => {
let gpe = GeneratePasswordEvent::from_parts(
// &idms_prox_write.qs_write,
ident,
target_uuid,
)
.map_err(|e| {
admin_error!(
err = ?e,
"Failed to begin internal_credential_set_message",
);
e
})?;
idms_prox_write
.generate_account_password(&gpe)
.and_then(|r| idms_prox_write.commit().map(|_| r))
.map(SetCredentialResponse::Token)
}
SetCredentialRequest::TotpGenerate => {
let gte = GenerateTotpEvent::from_parts(
// &idms_prox_write.qs_write,
ident,
target_uuid,
)
.map_err(|e| {
admin_error!(
err = ?e,
"Failed to begin internal_credential_set_message",
);
e
})?;
idms_prox_write
.generate_account_totp(&gte, ct)
.and_then(|r| idms_prox_write.commit().map(|_| r))
}
SetCredentialRequest::TotpVerify(uuid, chal) => {
let vte = VerifyTotpEvent::from_parts(
// &idms_prox_write.qs_write,
ident,
target_uuid,
uuid,
chal,
)
.map_err(|e| {
admin_error!(
err = ?e,
"Failed to begin internal_credential_set_message",
);
e
})?;
idms_prox_write
.verify_account_totp(&vte, ct)
.and_then(|r| idms_prox_write.commit().map(|_| r))
}
SetCredentialRequest::TotpAcceptSha1(uuid) => {
let aste =
AcceptSha1TotpEvent::from_parts(ident, target_uuid, uuid).map_err(|e| {
admin_error!(
err = ?e,
"Failed to begin internal_credential_set_message",
);
e
})?;
idms_prox_write
.accept_account_sha1_totp(&aste)
.and_then(|r| idms_prox_write.commit().map(|_| r))
}
SetCredentialRequest::TotpRemove => {
let rte = RemoveTotpEvent::from_parts(
// &idms_prox_write.qs_write,
ident,
target_uuid,
)
.map_err(|e| {
admin_error!(
err = ?e,
"Failed to begin internal_credential_set_message",
);
e
})?;
idms_prox_write
.remove_account_totp(&rte)
.and_then(|r| idms_prox_write.commit().map(|_| r))
}
/*
SetCredentialRequest::SecurityKeyBegin(label) => {
let wre = WebauthnInitRegisterEvent::from_parts(
// &idms_prox_write.qs_write,
ident,
target_uuid,
label,
)
.map_err(|e| {
admin_error!(
err = ?e,
"Failed to begin internal_credential_set_message",
);
e
})?;
idms_prox_write
.reg_account_webauthn_init(&wre, ct)
.and_then(|r| idms_prox_write.commit().map(|_| r))
}
SetCredentialRequest::SecurityKeyRegister(uuid, rpkc) => {
let wre = WebauthnDoRegisterEvent::from_parts(
// &idms_prox_write.qs_write,
ident,
target_uuid,
uuid,
rpkc,
)
.map_err(|e| {
admin_error!(
err = ?e,
"Failed to begin internal_credential_set_message",
);
e
})?;
idms_prox_write
.reg_account_webauthn_complete(&wre)
.and_then(|r| idms_prox_write.commit().map(|_| r))
}
SetCredentialRequest::SecurityKeyRemove(label) => {
let rwe = RemoveWebauthnEvent::from_parts(
// &idms_prox_write.qs_write,
ident,
target_uuid,
label,
)
.map_err(|e| {
admin_error!(
err = ?e,
"Failed to begin internal_credential_set_message",
);
e
})?;
idms_prox_write
.remove_account_webauthn(&rwe)
.and_then(|r| idms_prox_write.commit().map(|_| r))
}
*/
SetCredentialRequest::BackupCodeGenerate => {
let gbe = GenerateBackupCodeEvent::from_parts(
// &idms_prox_write.qs_write,
ident,
target_uuid,
)
.map_err(|e| {
admin_error!(
err = ?e,
"Failed to begin internal_credential_set_message",
);
e
})?;
idms_prox_write
.generate_backup_code(&gbe)
.and_then(|r| idms_prox_write.commit().map(|_| r))
.map(SetCredentialResponse::BackupCodes)
}
SetCredentialRequest::BackupCodeRemove => {
let rbe = RemoveBackupCodeEvent::from_parts(
// &idms_prox_write.qs_write,
ident,
target_uuid,
)
.map_err(|e| {
admin_error!(
err = ?e,
"Failed to begin internal_credential_set_message",
);
e
})?;
idms_prox_write
.remove_backup_code(&rbe)
.and_then(|r| idms_prox_write.commit().map(|_| r))
}
}
let gpe = GeneratePasswordEvent::from_parts(ident, target_uuid).map_err(|e| {
admin_error!(
err = ?e,
"Failed to begin handle_service_account_credential_generate",
);
e
})?;
idms_prox_write
.generate_account_password(&gpe)
.and_then(|r| idms_prox_write.commit().map(|_| r))
});
res
}
@ -833,7 +635,7 @@ impl QueryServerWriteV1 {
};
idms_prox_write
.commit_credential_update(session_token, ct)
.commit_credential_update(&session_token, ct)
.and_then(|tok| idms_prox_write.commit().map(|_| tok))
.map_err(|e| {
admin_error!(
@ -865,7 +667,7 @@ impl QueryServerWriteV1 {
};
idms_prox_write
.cancel_credential_update(session_token, ct)
.cancel_credential_update(&session_token, ct)
.and_then(|tok| idms_prox_write.commit().map(|_| tok))
.map_err(|e| {
admin_error!(
@ -878,6 +680,7 @@ impl QueryServerWriteV1 {
res
}
/*
#[instrument(
level = "info",
name = "idm_account_set_password",
@ -918,6 +721,44 @@ impl QueryServerWriteV1 {
});
res
}
*/
#[instrument(
level = "info",
name = "handle_service_account_into_person",
skip(self, uat, uuid_or_name, eventid)
fields(uuid = ?eventid)
)]
pub async fn handle_service_account_into_person(
&self,
uat: Option<String>,
uuid_or_name: String,
eventid: Uuid,
) -> Result<(), OperationError> {
let ct = duration_from_epoch_now();
let idms_prox_write = self.idms.proxy_write_async(ct).await;
let res = spanned!("actors::v1_write::handle<IdmServiceAccountIntoPerson>", {
let ident = idms_prox_write
.validate_and_parse_uat(uat.as_deref(), ct)
.and_then(|uat| idms_prox_write.process_uat_to_identity(&uat, ct))
.map_err(|e| {
admin_error!(err = ?e, "Invalid identity");
e
})?;
let target_uuid = idms_prox_write
.qs_write
.name_to_uuid(uuid_or_name.as_str())
.map_err(|e| {
admin_error!(err = ?e, "Error resolving id to target");
e
})?;
idms_prox_write
.service_account_into_person(&ident, target_uuid)
.and_then(|_| idms_prox_write.commit())
});
res
}
#[instrument(
level = "info",
@ -936,8 +777,6 @@ impl QueryServerWriteV1 {
let res = spanned!(
"actors::v1_write::handle<InternalRegenerateRadiusMessage>",
{
idms_prox_write.expire_mfareg_sessions(ct);
let ident = idms_prox_write
.validate_and_parse_uat(uat.as_deref(), ct)
.and_then(|uat| idms_prox_write.process_uat_to_identity(&uat, ct))
@ -989,9 +828,9 @@ impl QueryServerWriteV1 {
filter: Filter<FilterInvalid>,
eventid: Uuid,
) -> Result<(), OperationError> {
let idms_prox_write = self.idms.proxy_write_async(duration_from_epoch_now()).await;
let res = spanned!("actors::v1_write::handle<PurgeAttributeMessage>", {
let ct = duration_from_epoch_now();
let ct = duration_from_epoch_now();
let idms_prox_write = self.idms.proxy_write_async(ct).await;
spanned!("actors::v1_write::handle<PurgeAttributeMessage>", {
let ident = idms_prox_write
.validate_and_parse_uat(uat.as_deref(), ct)
.and_then(|uat| idms_prox_write.process_uat_to_identity(&uat, ct))
@ -1027,8 +866,7 @@ impl QueryServerWriteV1 {
.qs_write
.modify(&mdf)
.and_then(|_| idms_prox_write.commit().map(|_| ()))
});
res
})
}
#[instrument(
@ -1047,7 +885,7 @@ impl QueryServerWriteV1 {
eventid: Uuid,
) -> Result<(), OperationError> {
let idms_prox_write = self.idms.proxy_write_async(duration_from_epoch_now()).await;
let res = spanned!("actors::v1_write::handle<RemoveAttributeValuesMessage>", {
spanned!("actors::v1_write::handle<RemoveAttributeValuesMessage>", {
let ct = duration_from_epoch_now();
let ident = idms_prox_write
.validate_and_parse_uat(uat.as_deref(), ct)
@ -1091,8 +929,7 @@ impl QueryServerWriteV1 {
.qs_write
.modify(&mdf)
.and_then(|_| idms_prox_write.commit().map(|_| ()))
});
res
})
}
#[instrument(
@ -1118,10 +955,8 @@ impl QueryServerWriteV1 {
.map(|v| ProtoModify::Present(attr.clone(), v))
.collect(),
);
let res = self
.modify_from_parts(uat, &uuid_or_name, &proto_ml, filter)
.await;
res
self.modify_from_parts(uat, &uuid_or_name, &proto_ml, filter)
.await
}
#[instrument(
@ -1150,10 +985,8 @@ impl QueryServerWriteV1 {
)
.collect(),
);
let res = self
.modify_from_parts(uat, &uuid_or_name, &proto_ml, filter)
.await;
res
self.modify_from_parts(uat, &uuid_or_name, &proto_ml, filter)
.await
}
#[instrument(
@ -1175,121 +1008,8 @@ impl QueryServerWriteV1 {
// than relying on the proto ones.
let ml = ModifyList::new_append("ssh_publickey", Value::new_sshkey(tag, key));
let res = self
.modify_from_internal_parts(uat, &uuid_or_name, &ml, filter)
.await;
res
}
#[instrument(
level = "info",
name = "idm_account_person_extend",
skip(self, uat, uuid_or_name, eventid)
fields(uuid = ?eventid)
)]
pub async fn handle_idmaccountpersonextend(
&self,
uat: Option<String>,
uuid_or_name: String,
px: AccountPersonSet,
eventid: Uuid,
) -> Result<(), OperationError> {
let AccountPersonSet { mail, legalname } = px;
let mut mods: Vec<_> = Vec::with_capacity(4 + mail.as_ref().map(|v| v.len()).unwrap_or(0));
mods.push(Modify::Present("class".into(), Value::new_class("person")));
if let Some(s) = legalname {
mods.push(Modify::Purged("legalname".into()));
mods.push(Modify::Present("legalname".into(), Value::new_utf8(s)));
}
if let Some(mail) = mail {
mods.push(Modify::Purged("mail".into()));
let mut miter = mail.into_iter();
if let Some(m_primary) = miter.next() {
let v =
Value::new_email_address_primary_s(m_primary.as_str()).ok_or_else(|| {
OperationError::InvalidAttribute(format!(
"Invalid mail address {}",
m_primary
))
})?;
mods.push(Modify::Present("mail".into(), v));
}
for m in miter {
let v = Value::new_email_address_s(m.as_str()).ok_or_else(|| {
OperationError::InvalidAttribute(format!("Invalid mail address {}", m))
})?;
mods.push(Modify::Present("mail".into(), v));
}
}
let ml = ModifyList::new_list(mods);
let filter = filter_all!(f_eq("class", PartialValue::new_class("account")));
let res = self
.modify_from_internal_parts(uat, &uuid_or_name, &ml, filter)
.await;
res
}
#[instrument(
level = "info",
name = "idm_account_person_set",
skip(self, uat, uuid_or_name, eventid)
fields(uuid = ?eventid)
)]
pub async fn handle_idmaccountpersonset(
&self,
uat: Option<String>,
uuid_or_name: String,
px: AccountPersonSet,
eventid: Uuid,
) -> Result<(), OperationError> {
let AccountPersonSet { mail, legalname } = px;
let mut mods: Vec<_> = Vec::with_capacity(3 + mail.as_ref().map(|v| v.len()).unwrap_or(0));
if let Some(s) = legalname {
mods.push(Modify::Purged("legalname".into()));
mods.push(Modify::Present("legalname".into(), Value::new_utf8(s)));
}
if let Some(mail) = mail {
mods.push(Modify::Purged("mail".into()));
let mut miter = mail.into_iter();
if let Some(m_primary) = miter.next() {
let v =
Value::new_email_address_primary_s(m_primary.as_str()).ok_or_else(|| {
OperationError::InvalidAttribute(format!(
"Invalid mail address {}",
m_primary
))
})?;
mods.push(Modify::Present("mail".into(), v));
}
for m in miter {
let v = Value::new_email_address_s(m.as_str()).ok_or_else(|| {
OperationError::InvalidAttribute(format!("Invalid mail address {}", m))
})?;
mods.push(Modify::Present("mail".into(), v));
}
}
let ml = ModifyList::new_list(mods);
let filter = filter_all!(f_eq("class", PartialValue::new_class("account")));
let res = self
.modify_from_internal_parts(uat, &uuid_or_name, &ml, filter)
.await;
res
self.modify_from_internal_parts(uat, &uuid_or_name, &ml, filter)
.await
}
#[instrument(
@ -1333,10 +1053,8 @@ impl QueryServerWriteV1 {
let filter = filter_all!(f_eq("class", PartialValue::new_class("account")));
let res = self
.modify_from_internal_parts(uat, &uuid_or_name, &ml, filter)
.await;
res
self.modify_from_internal_parts(uat, &uuid_or_name, &ml, filter)
.await
}
#[instrument(
@ -1368,10 +1086,8 @@ impl QueryServerWriteV1 {
let filter = filter_all!(f_eq("class", PartialValue::new_class("group")));
let res = self
.modify_from_internal_parts(uat, &uuid_or_name, &ml, filter)
.await;
res
self.modify_from_internal_parts(uat, &uuid_or_name, &ml, filter)
.await
}
#[instrument(
@ -1390,8 +1106,6 @@ impl QueryServerWriteV1 {
let ct = duration_from_epoch_now();
let mut idms_prox_write = self.idms.proxy_write_async(ct).await;
let res = spanned!("actors::v1_write::handle<IdmAccountUnixSetCredMessage>", {
idms_prox_write.expire_mfareg_sessions(ct);
let ident = idms_prox_write
.validate_and_parse_uat(uat.as_deref(), ct)
.and_then(|uat| idms_prox_write.process_uat_to_identity(&uat, ct))

View file

@ -83,7 +83,7 @@ fn from_vec_dbval1(attr_val: Vec<DbValueV1>) -> Result<DbValueSetV2, OperationEr
}
})
.collect();
vs.map(|vs| DbValueSetV2::Iutf8(vs))
vs.map(DbValueSetV2::Iutf8)
}
Some(DbValueV1::Iname(_)) => {
let vs: Result<Vec<_>, _> = viter
@ -95,7 +95,7 @@ fn from_vec_dbval1(attr_val: Vec<DbValueV1>) -> Result<DbValueSetV2, OperationEr
}
})
.collect();
vs.map(|vs| DbValueSetV2::Iname(vs))
vs.map(DbValueSetV2::Iname)
}
Some(DbValueV1::Uuid(_)) => {
let vs: Result<Vec<_>, _> = viter
@ -107,7 +107,7 @@ fn from_vec_dbval1(attr_val: Vec<DbValueV1>) -> Result<DbValueSetV2, OperationEr
}
})
.collect();
vs.map(|vs| DbValueSetV2::Uuid(vs))
vs.map(DbValueSetV2::Uuid)
}
Some(DbValueV1::Bool(_)) => {
let vs: Result<Vec<_>, _> = viter
@ -119,7 +119,7 @@ fn from_vec_dbval1(attr_val: Vec<DbValueV1>) -> Result<DbValueSetV2, OperationEr
}
})
.collect();
vs.map(|vs| DbValueSetV2::Bool(vs))
vs.map(DbValueSetV2::Bool)
}
Some(DbValueV1::SyntaxType(_)) => {
let vs: Result<Vec<_>, _> = viter
@ -131,7 +131,7 @@ fn from_vec_dbval1(attr_val: Vec<DbValueV1>) -> Result<DbValueSetV2, OperationEr
}
})
.collect();
vs.map(|vs| DbValueSetV2::SyntaxType(vs))
vs.map(DbValueSetV2::SyntaxType)
}
Some(DbValueV1::IndexType(_)) => {
let vs: Result<Vec<_>, _> = viter
@ -143,7 +143,7 @@ fn from_vec_dbval1(attr_val: Vec<DbValueV1>) -> Result<DbValueSetV2, OperationEr
}
})
.collect();
vs.map(|vs| DbValueSetV2::IndexType(vs))
vs.map(DbValueSetV2::IndexType)
}
Some(DbValueV1::Reference(_)) => {
let vs: Result<Vec<_>, _> = viter
@ -155,7 +155,7 @@ fn from_vec_dbval1(attr_val: Vec<DbValueV1>) -> Result<DbValueSetV2, OperationEr
}
})
.collect();
vs.map(|vs| DbValueSetV2::Reference(vs))
vs.map(DbValueSetV2::Reference)
}
Some(DbValueV1::JsonFilter(_)) => {
let vs: Result<Vec<_>, _> = viter
@ -167,7 +167,7 @@ fn from_vec_dbval1(attr_val: Vec<DbValueV1>) -> Result<DbValueSetV2, OperationEr
}
})
.collect();
vs.map(|vs| DbValueSetV2::JsonFilter(vs))
vs.map(DbValueSetV2::JsonFilter)
}
Some(DbValueV1::Credential(_)) => {
let vs: Result<Vec<_>, _> = viter
@ -179,7 +179,7 @@ fn from_vec_dbval1(attr_val: Vec<DbValueV1>) -> Result<DbValueSetV2, OperationEr
}
})
.collect();
vs.map(|vs| DbValueSetV2::Credential(vs))
vs.map(DbValueSetV2::Credential)
}
Some(DbValueV1::SecretValue(_)) => {
let vs: Result<Vec<_>, _> = viter
@ -191,7 +191,7 @@ fn from_vec_dbval1(attr_val: Vec<DbValueV1>) -> Result<DbValueSetV2, OperationEr
}
})
.collect();
vs.map(|vs| DbValueSetV2::SecretValue(vs))
vs.map(DbValueSetV2::SecretValue)
}
Some(DbValueV1::SshKey(_)) => {
let vs: Result<Vec<_>, _> = viter
@ -203,7 +203,7 @@ fn from_vec_dbval1(attr_val: Vec<DbValueV1>) -> Result<DbValueSetV2, OperationEr
}
})
.collect();
vs.map(|vs| DbValueSetV2::SshKey(vs))
vs.map(DbValueSetV2::SshKey)
}
Some(DbValueV1::Spn(_, _)) => {
let vs: Result<Vec<_>, _> = viter
@ -215,7 +215,7 @@ fn from_vec_dbval1(attr_val: Vec<DbValueV1>) -> Result<DbValueSetV2, OperationEr
}
})
.collect();
vs.map(|vs| DbValueSetV2::Spn(vs))
vs.map(DbValueSetV2::Spn)
}
Some(DbValueV1::Uint32(_)) => {
let vs: Result<Vec<_>, _> = viter
@ -227,7 +227,7 @@ fn from_vec_dbval1(attr_val: Vec<DbValueV1>) -> Result<DbValueSetV2, OperationEr
}
})
.collect();
vs.map(|vs| DbValueSetV2::Uint32(vs))
vs.map(DbValueSetV2::Uint32)
}
Some(DbValueV1::Cid(_)) => {
let vs: Result<Vec<_>, _> = viter
@ -239,7 +239,7 @@ fn from_vec_dbval1(attr_val: Vec<DbValueV1>) -> Result<DbValueSetV2, OperationEr
}
})
.collect();
vs.map(|vs| DbValueSetV2::Cid(vs))
vs.map(DbValueSetV2::Cid)
}
Some(DbValueV1::NsUniqueId(_)) => {
let vs: Result<Vec<_>, _> = viter
@ -251,7 +251,7 @@ fn from_vec_dbval1(attr_val: Vec<DbValueV1>) -> Result<DbValueSetV2, OperationEr
}
})
.collect();
vs.map(|vs| DbValueSetV2::NsUniqueId(vs))
vs.map(DbValueSetV2::NsUniqueId)
}
Some(DbValueV1::DateTime(_)) => {
let vs: Result<Vec<_>, _> = viter
@ -263,7 +263,7 @@ fn from_vec_dbval1(attr_val: Vec<DbValueV1>) -> Result<DbValueSetV2, OperationEr
}
})
.collect();
vs.map(|vs| DbValueSetV2::DateTime(vs))
vs.map(DbValueSetV2::DateTime)
}
Some(DbValueV1::EmailAddress(_)) => {
let mut primary = None;
@ -309,7 +309,7 @@ fn from_vec_dbval1(attr_val: Vec<DbValueV1>) -> Result<DbValueSetV2, OperationEr
}
})
.collect();
vs.map(|vs| DbValueSetV2::Address(vs))
vs.map(DbValueSetV2::Address)
}
Some(DbValueV1::Url(_)) => {
let vs: Result<Vec<_>, _> = viter
@ -321,7 +321,7 @@ fn from_vec_dbval1(attr_val: Vec<DbValueV1>) -> Result<DbValueSetV2, OperationEr
}
})
.collect();
vs.map(|vs| DbValueSetV2::Url(vs))
vs.map(DbValueSetV2::Url)
}
Some(DbValueV1::OauthScope(_)) => {
let vs: Result<Vec<_>, _> = viter
@ -333,7 +333,7 @@ fn from_vec_dbval1(attr_val: Vec<DbValueV1>) -> Result<DbValueSetV2, OperationEr
}
})
.collect();
vs.map(|vs| DbValueSetV2::OauthScope(vs))
vs.map(DbValueSetV2::OauthScope)
}
Some(DbValueV1::OauthScopeMap(_)) => {
let vs: Result<Vec<_>, _> = viter
@ -345,7 +345,7 @@ fn from_vec_dbval1(attr_val: Vec<DbValueV1>) -> Result<DbValueSetV2, OperationEr
}
})
.collect();
vs.map(|vs| DbValueSetV2::OauthScopeMap(vs))
vs.map(DbValueSetV2::OauthScopeMap)
}
Some(DbValueV1::PrivateBinary(_)) => {
let vs: Result<Vec<_>, _> = viter
@ -357,7 +357,7 @@ fn from_vec_dbval1(attr_val: Vec<DbValueV1>) -> Result<DbValueSetV2, OperationEr
}
})
.collect();
vs.map(|vs| DbValueSetV2::PrivateBinary(vs))
vs.map(DbValueSetV2::PrivateBinary)
}
Some(DbValueV1::PublicBinary(_, _)) => {
let vs: Result<Vec<_>, _> = viter
@ -369,7 +369,7 @@ fn from_vec_dbval1(attr_val: Vec<DbValueV1>) -> Result<DbValueSetV2, OperationEr
}
})
.collect();
vs.map(|vs| DbValueSetV2::PublicBinary(vs))
vs.map(DbValueSetV2::PublicBinary)
}
Some(DbValueV1::RestrictedString(_)) => {
let vs: Result<Vec<_>, _> = viter
@ -381,7 +381,7 @@ fn from_vec_dbval1(attr_val: Vec<DbValueV1>) -> Result<DbValueSetV2, OperationEr
}
})
.collect();
vs.map(|vs| DbValueSetV2::RestrictedString(vs))
vs.map(DbValueSetV2::RestrictedString)
}
Some(DbValueV1::IntentToken { u: _, s: _ }) => {
let vs: Result<Vec<_>, _> = viter
@ -393,14 +393,14 @@ fn from_vec_dbval1(attr_val: Vec<DbValueV1>) -> Result<DbValueSetV2, OperationEr
}
})
.collect();
vs.map(|vs| DbValueSetV2::IntentToken(vs))
vs.map(DbValueSetV2::IntentToken)
}
// Neither of these should exist yet.
Some(DbValueV1::TrustedDeviceEnrollment { u: _ })
| Some(DbValueV1::AuthSession { u: _ })
| None => {
// Shiiiiii
assert!(false);
debug_assert!(false);
Err(OperationError::InvalidState)
}
}

View file

@ -325,12 +325,12 @@ pub trait IdlArcSqliteTransaction {
fn get_identry_raw(&self, idl: &IdList) -> Result<Vec<IdRawEntry>, OperationError>;
fn exists_idx(&mut self, attr: &str, itype: &IndexType) -> Result<bool, OperationError>;
fn exists_idx(&mut self, attr: &str, itype: IndexType) -> Result<bool, OperationError>;
fn get_idl(
&mut self,
attr: &str,
itype: &IndexType,
itype: IndexType,
idx_key: &str,
) -> Result<Option<IDLBitRange>, OperationError>;
@ -374,14 +374,14 @@ impl<'a> IdlArcSqliteTransaction for IdlArcSqliteReadTransaction<'a> {
get_identry_raw!(self, idl)
}
fn exists_idx(&mut self, attr: &str, itype: &IndexType) -> Result<bool, OperationError> {
fn exists_idx(&mut self, attr: &str, itype: IndexType) -> Result<bool, OperationError> {
exists_idx!(self, attr, itype)
}
fn get_idl(
&mut self,
attr: &str,
itype: &IndexType,
itype: IndexType,
idx_key: &str,
) -> Result<Option<IDLBitRange>, OperationError> {
get_idl!(self, attr, itype, idx_key)
@ -455,14 +455,14 @@ impl<'a> IdlArcSqliteTransaction for IdlArcSqliteWriteTransaction<'a> {
get_identry_raw!(self, idl)
}
fn exists_idx(&mut self, attr: &str, itype: &IndexType) -> Result<bool, OperationError> {
fn exists_idx(&mut self, attr: &str, itype: IndexType) -> Result<bool, OperationError> {
exists_idx!(self, attr, itype)
}
fn get_idl(
&mut self,
attr: &str,
itype: &IndexType,
itype: IndexType,
idx_key: &str,
) -> Result<Option<IDLBitRange>, OperationError> {
get_idl!(self, attr, itype, idx_key)
@ -557,7 +557,7 @@ impl<'a> IdlArcSqliteWriteTransaction<'a> {
spanned!("be::idl_arc_sqlite::commit<idl>", {
idl_cache.iter_mut_mark_clean().try_for_each(|(k, v)| {
match v {
Some(idl) => db.write_idl(k.a.as_str(), &k.i, k.k.as_str(), idl),
Some(idl) => db.write_idl(k.a.as_str(), k.i, k.k.as_str(), idl),
#[allow(clippy::unreachable)]
None => {
// Due to how we remove items, we always write an empty idl
@ -678,14 +678,14 @@ impl<'a> IdlArcSqliteWriteTransaction<'a> {
pub fn write_idl(
&mut self,
attr: &str,
itype: &IndexType,
itype: IndexType,
idx_key: &str,
idl: &IDLBitRange,
) -> Result<(), OperationError> {
spanned!("be::idl_arc_sqlite::write_idl", {
let cache_key = IdlCacheKey {
a: attr.into(),
i: itype.clone(),
i: itype,
k: idx_key.into(),
};
// On idl == 0 the db will remove this, and synthesise an empty IdList on a miss
@ -1028,7 +1028,7 @@ impl<'a> IdlArcSqliteWriteTransaction<'a> {
})
}
pub fn create_idx(&self, attr: &str, itype: &IndexType) -> Result<(), OperationError> {
pub fn create_idx(&self, attr: &str, itype: IndexType) -> Result<(), OperationError> {
// We don't need to affect this, so pass it down.
self.db.create_idx(attr, itype)
}

View file

@ -209,7 +209,7 @@ pub trait IdlSqliteTransaction {
}
}
fn exists_idx(&self, attr: &str, itype: &IndexType) -> Result<bool, OperationError> {
fn exists_idx(&self, attr: &str, itype: IndexType) -> Result<bool, OperationError> {
let tname = format!("idx_{}_{}", itype.as_idx_str(), attr);
self.exists_table(&tname)
}
@ -217,7 +217,7 @@ pub trait IdlSqliteTransaction {
fn get_idl(
&self,
attr: &str,
itype: &IndexType,
itype: IndexType,
idx_key: &str,
) -> Result<Option<IDLBitRange>, OperationError> {
spanned!("be::idl_sqlite::get_idl", {
@ -766,7 +766,7 @@ impl IdlSqliteWriteTransaction {
pub fn write_idl(
&self,
attr: &str,
itype: &IndexType,
itype: IndexType,
idx_key: &str,
idl: &IDLBitRange,
) -> Result<(), OperationError> {
@ -934,7 +934,7 @@ impl IdlSqliteWriteTransaction {
}
}
pub fn create_idx(&self, attr: &str, itype: &IndexType) -> Result<(), OperationError> {
pub fn create_idx(&self, attr: &str, itype: IndexType) -> Result<(), OperationError> {
// Is there a better way than formatting this? I can't seem
// to template into the str.
//

View file

@ -37,7 +37,7 @@ impl<'a> IdxKeyRef<'a> {
pub fn as_key(&self) -> IdxKey {
IdxKey {
attr: self.attr.into(),
itype: self.itype.clone(),
itype: *self.itype,
}
}
}
@ -94,7 +94,7 @@ pub struct IdlCacheKey {
#[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq, Hash)]
pub struct IdlCacheKeyRef<'a> {
pub a: &'a str,
pub i: &'a IndexType,
pub i: IndexType,
pub k: &'a str,
}
@ -121,7 +121,7 @@ impl IdlCacheKeyToRef for IdlCacheKey {
fn keyref(&self) -> IdlCacheKeyRef<'_> {
IdlCacheKeyRef {
a: self.a.as_str(),
i: &self.i,
i: self.i,
k: self.k.as_str(),
}
}

View file

@ -98,6 +98,7 @@ impl BackendConfig {
}
}
#[cfg(test)]
pub(crate) fn new_test() -> Self {
BackendConfig {
pool_size: 1,
@ -188,7 +189,7 @@ pub trait BackendTransaction {
// Get the idl for this
match self
.get_idlayer()
.get_idl(attr, &IndexType::Equality, &idx_key)?
.get_idl(attr, IndexType::Equality, &idx_key)?
{
Some(idl) => (
IdList::Indexed(idl),
@ -208,7 +209,7 @@ pub trait BackendTransaction {
// Get the idl for this
match self
.get_idlayer()
.get_idl(attr, &IndexType::SubString, &idx_key)?
.get_idl(attr, IndexType::SubString, &idx_key)?
{
Some(idl) => (
IdList::Indexed(idl),
@ -224,11 +225,7 @@ pub trait BackendTransaction {
FilterResolved::Pres(attr, idx) => {
if idx.is_some() {
// Get the idl for this
match self.get_idlayer().get_idl(
attr,
&IndexType::Presence,
&"_".to_string(),
)? {
match self.get_idlayer().get_idl(attr, IndexType::Presence, "_")? {
Some(idl) => (IdList::Indexed(idl), FilterPlan::PresIndexed(attr.clone())),
None => (IdList::AllIds, FilterPlan::PresCorrupt(attr.clone())),
}
@ -993,7 +990,7 @@ impl<'a> BackendWriteTransaction<'a> {
// This auto compresses.
let ruv_idl = IDLBitRange::from_iter(c_entries.iter().map(|e| e.get_id()));
self.get_ruv().insert_change(cid.clone(), ruv_idl)?;
self.get_ruv().insert_change(cid, ruv_idl)?;
idlayer.write_identries(c_entries.iter())?;
@ -1036,7 +1033,7 @@ impl<'a> BackendWriteTransaction<'a> {
// All good, lets update the RUV.
// This auto compresses.
let ruv_idl = IDLBitRange::from_iter(post_entries.iter().map(|e| e.get_id()));
self.get_ruv().insert_change(cid.clone(), ruv_idl)?;
self.get_ruv().insert_change(cid, ruv_idl)?;
// Now, given the list of id's, update them
self.get_idlayer().write_identries(post_entries.iter())?;
@ -1277,17 +1274,17 @@ impl<'a> BackendWriteTransaction<'a> {
let idxmeta = unsafe { &(*(&self.idxmeta.idxkeys as *const _)) };
let idx_diff = Entry::idx_diff(&(*idxmeta), pre, post);
let idx_diff = Entry::idx_diff(idxmeta, pre, post);
idx_diff.iter()
idx_diff.into_iter()
.try_for_each(|act| {
match act {
Ok((attr, itype, idx_key)) => {
trace!("Adding {:?} idx -> {:?}: {:?}", itype, attr, idx_key);
match idlayer.get_idl(attr, itype, idx_key)? {
match idlayer.get_idl(attr, itype, &idx_key)? {
Some(mut idl) => {
idl.insert_id(e_id);
idlayer.write_idl(attr, itype, idx_key, &idl)
idlayer.write_idl(attr, itype, &idx_key, &idl)
}
None => {
admin_error!(
@ -1300,10 +1297,10 @@ impl<'a> BackendWriteTransaction<'a> {
}
Err((attr, itype, idx_key)) => {
trace!("Removing {:?} idx -> {:?}: {:?}", itype, attr, idx_key);
match idlayer.get_idl(attr, itype, idx_key)? {
match idlayer.get_idl(attr, itype, &idx_key)? {
Some(mut idl) => {
idl.remove_id(e_id);
idlayer.write_idl(attr, itype, idx_key, &idl)
idlayer.write_idl(attr, itype, &idx_key, &idl)
}
None => {
admin_error!(
@ -1338,7 +1335,7 @@ impl<'a> BackendWriteTransaction<'a> {
if idx_table_set.contains(&tname) {
None
} else {
Some((ikey.attr.clone(), ikey.itype.clone()))
Some((ikey.attr.clone(), ikey.itype))
}
})
.collect();
@ -1360,7 +1357,7 @@ impl<'a> BackendWriteTransaction<'a> {
self.idxmeta
.idxkeys
.keys()
.try_for_each(|ikey| idlayer.create_idx(&ikey.attr, &ikey.itype))
.try_for_each(|ikey| idlayer.create_idx(&ikey.attr, ikey.itype))
}
pub fn upgrade_reindex(&self, v: i64) -> Result<(), OperationError> {
@ -1434,7 +1431,7 @@ impl<'a> BackendWriteTransaction<'a> {
pub fn load_test_idl(
&self,
attr: &String,
itype: &IndexType,
itype: IndexType,
idx_key: &String,
) -> Result<Option<IDLBitRange>, OperationError> {
self.get_idlayer().get_idl(attr, itype, idx_key)
@ -1872,7 +1869,7 @@ mod tests {
macro_rules! idl_state {
($be:expr, $attr:expr, $itype:expr, $idx_key:expr, $expect:expr) => {{
let t_idl = $be
.load_test_idl(&$attr.to_string(), &$itype, &$idx_key.to_string())
.load_test_idl(&$attr.to_string(), $itype, &$idx_key.to_string())
.expect("IdList Load failed");
let t = $expect.map(|v: Vec<u64>| IDLBitRange::from_iter(v));
assert_eq!(t_idl, t);
@ -2335,7 +2332,7 @@ mod tests {
let uuid_p_idl = be
.load_test_idl(
&"not_indexed".to_string(),
&IndexType::Presence,
IndexType::Presence,
&"_".to_string(),
)
.unwrap(); // unwrap the result

View file

@ -448,7 +448,7 @@ pub const JSON_IDM_ACP_ACCOUNT_WRITE_PRIV_V1: &str = r#"{
],
"name": ["idm_acp_account_write_priv"],
"uuid": ["00000000-0000-0000-0000-ffffff000011"],
"description": ["Builtin IDM Control for managing accounts."],
"description": ["Builtin IDM Control for managing all accounts (both person and service)."],
"acp_receiver": [
"{\"eq\":[\"memberof\",\"00000000-0000-0000-0000-000000000006\"]}"
],
@ -495,7 +495,7 @@ pub const JSON_IDM_ACP_ACCOUNT_MANAGE_PRIV_V1: &str = r#"{
"devicekeys"
],
"acp_create_class": [
"object", "account"
"object", "account", "service_account"
]
}
}"#;
@ -605,7 +605,7 @@ pub const JSON_IDM_ACP_HP_ACCOUNT_WRITE_PRIV_V1: &str = r#"{
],
"name": ["idm_acp_hp_account_write_priv"],
"uuid": ["00000000-0000-0000-0000-ffffff000016"],
"description": ["Builtin IDM Control for managing high privilege accounts."],
"description": ["Builtin IDM Control for managing high privilege accounts (both person and service)."],
"acp_receiver": [
"{\"eq\":[\"memberof\",\"00000000-0000-0000-0000-000000000009\"]}"
],
@ -922,7 +922,7 @@ pub const JSON_IDM_ACP_HP_ACCOUNT_MANAGE_PRIV_V1: &str = r#"{
"devicekeys"
],
"acp_create_class": [
"object", "account"
"object", "account", "service_account"
]
}
}"#;
@ -1224,3 +1224,33 @@ pub const JSON_IDM_HP_ACP_OAUTH2_MANAGE_PRIV_V1: &str = r#"{
"acp_create_class": ["oauth2_resource_server", "oauth2_resource_server_basic", "object"]
}
}"#;
pub const JSON_IDM_HP_ACP_SERVICE_ACCOUNT_INTO_PERSON_MIGRATE_V1: &str = r#"{
"attrs": {
"class": [
"object",
"access_control_search",
"access_control_profile",
"access_control_modify"
],
"name": ["idm_hp_acp_service_account_into_person_migrate"],
"uuid": ["00000000-0000-0000-0000-ffffff000042"],
"description": ["Builtin IDM Control allowing service accounts to be migrated into persons"],
"acp_receiver": [
"{\"eq\":[\"memberof\",\"00000000-0000-0000-0000-000000000034\"]}"
],
"acp_targetscope": [
"{\"and\": [{\"eq\": [\"class\",\"account\"]}, {\"andnot\": {\"or\": [{\"eq\": [\"class\", \"tombstone\"]}, {\"eq\": [\"class\", \"recycled\"]}]}}]}"
],
"acp_search_attr": [
"class", "name", "uuid"
],
"acp_modify_removedattr": [
"class"
],
"acp_modify_presentattr": [
"class"
],
"acp_modify_class": ["service_account", "person"]
}
}"#;

View file

@ -3,7 +3,7 @@
/// Builtin System Admin account.
pub const JSON_ADMIN_V1: &str = r#"{
"attrs": {
"class": ["account", "memberof", "object"],
"class": ["account", "service_account", "memberof", "object"],
"name": ["admin"],
"uuid": ["00000000-0000-0000-0000-000000000000"],
"description": ["Builtin System Admin account."],
@ -14,7 +14,7 @@ pub const JSON_ADMIN_V1: &str = r#"{
/// Builtin IDM Admin account.
pub const JSON_IDM_ADMIN_V1: &str = r#"{
"attrs": {
"class": ["account", "memberof", "object"],
"class": ["account", "service_account", "memberof", "object"],
"name": ["idm_admin"],
"uuid": ["00000000-0000-0000-0000-000000000018"],
"description": ["Builtin IDM Admin account."],
@ -403,6 +403,18 @@ pub const JSON_IDM_HP_OAUTH2_MANAGE_PRIV_V1: &str = r#"{
}
}"#;
pub const JSON_IDM_HP_SERVICE_ACCOUNT_INTO_PERSON_MIGRATE_PRIV: &str = r#"{
"attrs": {
"class": ["group", "object"],
"name": ["idm_hp_service_account_into_person_migrate_priv"],
"uuid": ["00000000-0000-0000-0000-000000000034"],
"description": ["Builtin IDM Group for allowing migrations of service accounts into persons"],
"member": [
"00000000-0000-0000-0000-000000000019"
]
}
}"#;
/// This must be the last group to init to include the UUID of the other high priv groups.
pub const JSON_IDM_HIGH_PRIVILEGE_V1: &str = r#"{
"attrs": {
@ -437,6 +449,7 @@ pub const JSON_IDM_HIGH_PRIVILEGE_V1: &str = r#"{
"00000000-0000-0000-0000-000000000027",
"00000000-0000-0000-0000-000000000031",
"00000000-0000-0000-0000-000000000032",
"00000000-0000-0000-0000-000000000034",
"00000000-0000-0000-0000-000000001000"
]
}
@ -447,7 +460,7 @@ pub const JSON_SYSTEM_INFO_V1: &str = r#"{
"class": ["object", "system_info", "system"],
"uuid": ["00000000-0000-0000-0000-ffffff000001"],
"description": ["System (local) info and metadata object."],
"version": ["6"]
"version": ["7"]
}
}"#;
@ -464,7 +477,7 @@ pub const JSON_DOMAIN_INFO_V1: &str = r#"{
// Anonymous should be the last object in the range here.
pub const JSON_ANONYMOUS_V1: &str = r#"{
"attrs": {
"class": ["account", "object"],
"class": ["account", "service_account", "object"],
"name": ["anonymous"],
"uuid": ["00000000-0000-0000-0000-ffffffffffff"],
"description": ["Anonymous access account."],

View file

@ -1077,6 +1077,7 @@ pub const JSON_SCHEMA_CLASS_GROUP: &str = r#"
}
}
"#;
pub const JSON_SCHEMA_CLASS_ACCOUNT: &str = r#"
{
"attrs": {
@ -1110,6 +1111,38 @@ pub const JSON_SCHEMA_CLASS_ACCOUNT: &str = r#"
],
"uuid": [
"00000000-0000-0000-0000-ffff00000046"
],
"systemsupplements": [
"person",
"service_account"
]
}
}
"#;
pub const JSON_SCHEMA_CLASS_SERVICE_ACCOUNT: &str = r#"
{
"attrs": {
"class": [
"object",
"system",
"classtype"
],
"description": [
"Object representation of service account"
],
"classname": [
"service_account"
],
"systemmay": [
"mail",
"primary_credential"
],
"uuid": [
"00000000-0000-0000-0000-ffff00000106"
],
"systemexcludes": [
"person"
]
}
}
@ -1172,6 +1205,9 @@ pub const JSON_SCHEMA_CLASS_POSIXGROUP: &str = r#"
],
"uuid": [
"00000000-0000-0000-0000-ffff00000058"
],
"systemsupplements": [
"group"
]
}
}
@ -1200,12 +1236,15 @@ pub const JSON_SCHEMA_CLASS_POSIXACCOUNT: &str = r#"
],
"uuid": [
"00000000-0000-0000-0000-ffff00000057"
],
"systemsupplements": [
"account"
]
}
}
"#;
pub const JSON_SCHEMA_CLASS_SYSTEM_CONFIG: &str = &r#"
pub const JSON_SCHEMA_CLASS_SYSTEM_CONFIG: &str = r#"
{
"attrs": {
"class": [

View file

@ -42,9 +42,10 @@ pub const _UUID_IDM_RADIUS_SECRET_READ_PRIV_V1: Uuid =
uuid!("00000000-0000-0000-0000-000000000032");
pub const _UUID_IDM_RADIUS_SECRET_WRITE_PRIV_V1: Uuid =
uuid!("00000000-0000-0000-0000-000000000031");
pub const _UUID_IDM_PEOPLE_SELF_WRITE_MAIL_PRIV: Uuid =
uuid!("00000000-0000-0000-0000-000000000033");
pub const _UUID_IDM_HP_SERVICE_ACCOUNT_INTO_PERSON_MIGRATE_PRIV: Uuid =
uuid!("00000000-0000-0000-0000-000000000034");
//
pub const _UUID_IDM_HIGH_PRIVILEGE: Uuid = uuid!("00000000-0000-0000-0000-000000001000");
@ -174,6 +175,13 @@ pub const _UUID_SCHEMA_ATTR_DOMAIN_DISPLAY_NAME: Uuid =
pub const _UUID_SCHEMA_ATTR_PASSKEYS: Uuid = uuid!("00000000-0000-0000-0000-ffff00000099");
pub const _UUID_SCHEMA_ATTR_DEVICEKEYS: Uuid = uuid!("00000000-0000-0000-0000-ffff00000100");
pub const UUID_SCHEMA_ATTR_SYSTEMSUPPLEMENTS: Uuid = uuid!("00000000-0000-0000-0000-ffff00000101");
pub const UUID_SCHEMA_ATTR_SUPPLEMENTS: Uuid = uuid!("00000000-0000-0000-0000-ffff00000102");
pub const UUID_SCHEMA_ATTR_SYSTEMEXCLUDES: Uuid = uuid!("00000000-0000-0000-0000-ffff00000103");
pub const UUID_SCHEMA_ATTR_EXCLUDES: Uuid = uuid!("00000000-0000-0000-0000-ffff00000104");
pub const UUID_SCHEMA_ATTR_SCOPE: Uuid = uuid!("00000000-0000-0000-0000-ffff00000105");
pub const UUID_SCHEMA_CLASS_SERVICE_ACCOUNT: Uuid = uuid!("00000000-0000-0000-0000-ffff00000106");
// System and domain infos
// I'd like to strongly criticise william of the past for making poor choices about these allocations.
pub const UUID_SYSTEM_INFO: Uuid = uuid!("00000000-0000-0000-0000-ffffff000001");
@ -246,6 +254,8 @@ pub const _UUID_IDM_ACP_RADIUS_SECRET_WRITE_PRIV_V1: Uuid =
uuid!("00000000-0000-0000-0000-ffffff000040");
pub const _UUID_IDM_PEOPLE_SELF_ACP_WRITE_MAIL_V1: Uuid =
uuid!("00000000-0000-0000-0000-ffffff000041");
pub const _UUID_IDM_HP_ACP_SERVICE_ACCOUNT_INTO_PERSON_MIGRATE_V1: Uuid =
uuid!("00000000-0000-0000-0000-ffffff000042");
// End of system ranges
pub const UUID_DOES_NOT_EXIST: Uuid = uuid!("00000000-0000-0000-0000-fffffffffffe");

View file

@ -133,7 +133,7 @@ impl Password {
let mut key: Vec<u8> = (0..PBKDF2_KEY_LEN).map(|_| 0).collect();
let start = Instant::now();
let _ = pbkdf2_hmac(
pbkdf2_hmac(
input.as_slice(),
salt.as_slice(),
pbkdf2_cost,
@ -215,7 +215,7 @@ impl Password {
}
}
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BackupCodes {
code_set: HashSet<String>,
}

View file

@ -14,14 +14,14 @@ use kanidm_proto::v1::TotpSecret as ProtoTotp;
const SECRET_SIZE_BYTES: usize = 8;
pub const TOTP_DEFAULT_STEP: u64 = 30;
#[derive(Debug, PartialEq)]
#[derive(Debug, PartialEq, Eq)]
pub enum TotpError {
OpenSSLError,
HmacError,
TimeError,
}
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TotpAlgo {
Sha1,
Sha256,
@ -58,7 +58,7 @@ impl TotpAlgo {
}
/// <https://tools.ietf.org/html/rfc6238> which relies on <https://tools.ietf.org/html/rfc4226>
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Totp {
secret: Vec<u8>,
pub(crate) step: u64,

View file

@ -663,30 +663,84 @@ impl<STATE> Entry<EntryInvalid, STATE> {
// Do we have extensible?
let extensible = ne.attribute_equality("class", &CLASS_EXTENSIBLE);
let entry_classes = ne.get_ava_set("class").ok_or(SchemaError::NoClassFound)?;
let entry_classes = ne.get_ava_set("class").ok_or_else(|| {
admin_debug!("Attribute 'class' missing from entry");
SchemaError::NoClassFound
})?;
let mut invalid_classes = Vec::with_capacity(0);
let mut classes: Vec<&SchemaClass> = Vec::with_capacity(entry_classes.len());
match entry_classes.as_iutf8_iter() {
Some(cls_iter) => cls_iter.for_each(|s| match schema_classes.get(s) {
Some(x) => classes.push(x),
None => {
admin_debug!("invalid class: {:?}", s);
invalid_classes.push(s.to_string())
}
}),
None => {
admin_debug!("corrupt class attribute in: {:?}", entry_classes);
invalid_classes.push("corrupt class attribute".to_string())
}
// We need to keep the btreeset of entry classes here so we can check the
// requires and excludes.
let entry_classes = if let Some(ec) = entry_classes.as_iutf8_set() {
ec.iter()
.for_each(|s| match schema_classes.get(s.as_str()) {
Some(x) => classes.push(x),
None => {
admin_debug!("invalid class: {:?}", s);
invalid_classes.push(s.to_string())
}
});
ec
} else {
admin_debug!("corrupt class attribute");
return Err(SchemaError::NoClassFound);
};
if !invalid_classes.is_empty() {
// lrequest_error!("Class on entry not found in schema?");
return Err(SchemaError::InvalidClass(invalid_classes));
};
// Now determine the set of excludes and requires we have, and then
// assert we don't violate them.
let supplements_classes: Vec<_> = classes
.iter()
.flat_map(|cls| cls.systemsupplements.iter().chain(cls.supplements.iter()))
.collect();
// So long as one supplement is present we can continue.
let valid_supplements = if supplements_classes.is_empty() {
// No need to check.
true
} else {
supplements_classes
.iter()
.any(|class| entry_classes.contains(class.as_str()))
};
if !valid_supplements {
admin_warn!(
"Validation error, the following possible supplement classes are missing - {:?}",
supplements_classes
);
let supplements_classes =
supplements_classes.iter().map(|s| s.to_string()).collect();
return Err(SchemaError::SupplementsNotSatisfied(supplements_classes));
}
let excludes_classes: Vec<_> = classes
.iter()
.flat_map(|cls| cls.systemexcludes.iter().chain(cls.excludes.iter()))
.collect();
let mut invalid_excludes = Vec::with_capacity(0);
excludes_classes.iter().for_each(|class| {
if entry_classes.contains(class.as_str()) {
invalid_excludes.push(class.to_string())
}
});
if !invalid_excludes.is_empty() {
admin_warn!(
"Validation error, the following excluded classes are present - {:?}",
invalid_excludes
);
return Err(SchemaError::ExcludesNotSatisfied(invalid_excludes));
}
// What this is really doing is taking a set of classes, and building an
// "overall" class that describes this exact object for checking. IE we
// build a super must/may set from the small class must/may sets.
@ -998,7 +1052,7 @@ impl Entry<EntrySealed, EntryNew> {
}
type IdxDiff<'a> =
Vec<Result<(&'a AttrString, &'a IndexType, String), (&'a AttrString, &'a IndexType, String)>>;
Vec<Result<(&'a AttrString, IndexType, String), (&'a AttrString, IndexType, String)>>;
impl<VALID> Entry<VALID, EntryCommitted> {
/// If this entry has ever been commited to disk, retrieve it's database id number.
@ -1238,11 +1292,11 @@ impl Entry<EntrySealed, EntryCommitted> {
// We generate these keys out of the valueset now.
vs.generate_idx_eq_keys()
.into_iter()
.map(|idx_key| Err((&ikey.attr, &ikey.itype, idx_key)))
.map(|idx_key| Err((&ikey.attr, ikey.itype, idx_key)))
.collect()
}
IndexType::Presence => {
vec![Err((&ikey.attr, &ikey.itype, "_".to_string()))]
vec![Err((&ikey.attr, ikey.itype, "_".to_string()))]
}
IndexType::SubString => Vec::new(),
};
@ -1264,10 +1318,10 @@ impl Entry<EntrySealed, EntryCommitted> {
IndexType::Equality => vs
.generate_idx_eq_keys()
.into_iter()
.map(|idx_key| Ok((&ikey.attr, &ikey.itype, idx_key)))
.map(|idx_key| Ok((&ikey.attr, ikey.itype, idx_key)))
.collect(),
IndexType::Presence => {
vec![Ok((&ikey.attr, &ikey.itype, "_".to_string()))]
vec![Ok((&ikey.attr, ikey.itype, "_".to_string()))]
}
IndexType::SubString => Vec::new(),
};
@ -1301,11 +1355,11 @@ impl Entry<EntrySealed, EntryCommitted> {
pre_vs
.generate_idx_eq_keys()
.into_iter()
.map(|idx_key| Err((&ikey.attr, &ikey.itype, idx_key)))
.map(|idx_key| Err((&ikey.attr, ikey.itype, idx_key)))
.collect()
}
IndexType::Presence => {
vec![Err((&ikey.attr, &ikey.itype, "_".to_string()))]
vec![Err((&ikey.attr, ikey.itype, "_".to_string()))]
}
IndexType::SubString => Vec::new(),
};
@ -1320,11 +1374,11 @@ impl Entry<EntrySealed, EntryCommitted> {
post_vs
.generate_idx_eq_keys()
.into_iter()
.map(|idx_key| Ok((&ikey.attr, &ikey.itype, idx_key)))
.map(|idx_key| Ok((&ikey.attr, ikey.itype, idx_key)))
.collect()
}
IndexType::Presence => {
vec![Ok((&ikey.attr, &ikey.itype, "_".to_string()))]
vec![Ok((&ikey.attr, ikey.itype, "_".to_string()))]
}
IndexType::SubString => Vec::new(),
};
@ -1394,11 +1448,11 @@ impl Entry<EntrySealed, EntryCommitted> {
IndexType::Equality => {
removed_vs
.into_iter()
.map(|idx_key| Err((&ikey.attr, &ikey.itype, idx_key)))
.map(|idx_key| Err((&ikey.attr, ikey.itype, idx_key)))
.for_each(|v| diff.push(v));
added_vs
.into_iter()
.map(|idx_key| Ok((&ikey.attr, &ikey.itype, idx_key)))
.map(|idx_key| Ok((&ikey.attr, ikey.itype, idx_key)))
.for_each(|v| diff.push(v));
}
IndexType::Presence => {
@ -1456,7 +1510,7 @@ impl Entry<EntrySealed, EntryCommitted> {
let cid = attrs
.get("last_modified_cid")
.and_then(|vs| vs.as_cid_set())
.and_then(|set| set.iter().cloned().next())?;
.and_then(|set| set.iter().next().cloned())?;
let eclog = EntryChangelog::new_without_schema(cid, attrs.clone());
@ -1784,6 +1838,11 @@ impl<VALID, STATE> Entry<VALID, STATE> {
self.attrs.get(attr)
}
#[inline(always)]
pub fn get_ava_as_iutf8_iter(&self, attr: &str) -> Option<impl Iterator<Item = &str>> {
self.attrs.get(attr).and_then(|vs| vs.as_iutf8_iter())
}
#[inline(always)]
pub fn get_ava_as_oauthscopes(&self, attr: &str) -> Option<impl Iterator<Item = &str>> {
self.attrs.get(attr).and_then(|vs| vs.as_oauthscope_iter())
@ -2611,7 +2670,7 @@ mod tests {
del_r[0]
== Err((
&AttrString::from("userid"),
&IndexType::Equality,
IndexType::Equality,
"william".to_string()
))
);
@ -2619,7 +2678,7 @@ mod tests {
del_r[1]
== Err((
&AttrString::from("userid"),
&IndexType::Presence,
IndexType::Presence,
"_".to_string()
))
);
@ -2632,7 +2691,7 @@ mod tests {
add_r[0]
== Ok((
&AttrString::from("userid"),
&IndexType::Equality,
IndexType::Equality,
"william".to_string()
))
);
@ -2640,7 +2699,7 @@ mod tests {
add_r[1]
== Ok((
&AttrString::from("userid"),
&IndexType::Presence,
IndexType::Presence,
"_".to_string()
))
);
@ -2657,7 +2716,7 @@ mod tests {
add_a_r[0]
== Ok((
&AttrString::from("extra"),
&IndexType::Equality,
IndexType::Equality,
"test".to_string()
))
);
@ -2668,7 +2727,7 @@ mod tests {
del_a_r[0]
== Err((
&AttrString::from("extra"),
&IndexType::Equality,
IndexType::Equality,
"test".to_string()
))
);
@ -2681,7 +2740,7 @@ mod tests {
chg_r[1]
== Err((
&AttrString::from("userid"),
&IndexType::Equality,
IndexType::Equality,
"william".to_string()
))
);
@ -2690,7 +2749,7 @@ mod tests {
chg_r[0]
== Ok((
&AttrString::from("userid"),
&IndexType::Equality,
IndexType::Equality,
"claire".to_string()
))
);

View file

@ -162,7 +162,7 @@ pub enum FilterResolved {
AndNot(Box<FilterResolved>, Option<NonZeroU8>),
}
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FilterInvalid {
inner: FilterComp,
}
@ -172,7 +172,7 @@ pub struct FilterValid {
inner: FilterComp,
}
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FilterValidResolved {
inner: FilterResolved,
}

View file

@ -1,6 +1,6 @@
use crate::credential::BackupCodes;
use crate::entry::{Entry, EntryCommitted, EntryReduced, EntrySealed};
use crate::prelude::*;
use crate::schema::SchemaTransaction;
use kanidm_proto::v1::OperationError;
use kanidm_proto::v1::{AuthType, UserAuthToken};
@ -12,13 +12,13 @@ use webauthn_rs::prelude::Passkey as PasskeyV4;
use crate::constants::UUID_ANONYMOUS;
use crate::credential::policy::CryptoPolicy;
use crate::credential::totp::Totp;
use crate::credential::{softlock::CredSoftLockPolicy, Credential};
use crate::idm::group::Group;
use crate::idm::server::IdmServerProxyWriteTransaction;
use crate::modify::{ModifyInvalid, ModifyList};
use crate::value::{IntentTokenState, PartialValue, Value};
use std::collections::BTreeMap;
use std::collections::{BTreeMap, BTreeSet};
use std::time::Duration;
use time::OffsetDateTime;
use uuid::Uuid;
@ -318,39 +318,6 @@ impl Account {
}
}
pub(crate) fn gen_totp_mod(
&self,
token: Totp,
) -> Result<ModifyList<ModifyInvalid>, OperationError> {
match &self.primary {
// Change the cred
Some(primary) => {
let ncred = primary.update_totp(token);
let vcred = Value::new_credential("primary", ncred);
Ok(ModifyList::new_purge_and_set("primary_credential", vcred))
}
None => {
// No credential exists, we can't supplementy it.
Err(OperationError::InvalidState)
}
}
}
pub(crate) fn gen_totp_remove_mod(&self) -> Result<ModifyList<ModifyInvalid>, OperationError> {
match &self.primary {
// Change the cred
Some(primary) => {
let ncred = primary.remove_totp();
let vcred = Value::new_credential("primary", ncred);
Ok(ModifyList::new_purge_and_set("primary_credential", vcred))
}
None => {
// No credential exists, we can't remove what is not real.
Err(OperationError::InvalidState)
}
}
}
pub(crate) fn gen_webauthn_counter_mod(
&mut self,
auth_result: &AuthenticationResult,
@ -390,29 +357,6 @@ impl Account {
}
}
pub(crate) fn gen_backup_code_mod(
&self,
backup_codes: BackupCodes,
) -> Result<ModifyList<ModifyInvalid>, OperationError> {
match &self.primary {
// Change the cred
Some(primary) => {
let r_ncred = primary.update_backup_code(backup_codes);
match r_ncred {
Ok(ncred) => {
let vcred = Value::new_credential("primary", ncred);
Ok(ModifyList::new_purge_and_set("primary_credential", vcred))
}
Err(e) => Err(e),
}
}
None => {
// No credential exists, we can't supplementy it.
Err(OperationError::InvalidState)
}
}
}
pub(crate) fn invalidate_backup_code_mod(
self,
code_to_remove: &str,
@ -436,28 +380,6 @@ impl Account {
}
}
pub(crate) fn gen_backup_code_remove_mod(
&self,
) -> Result<ModifyList<ModifyInvalid>, OperationError> {
match &self.primary {
// Change the cred
Some(primary) => {
let r_ncred = primary.remove_backup_code();
match r_ncred {
Ok(ncred) => {
let vcred = Value::new_credential("primary", ncred);
Ok(ModifyList::new_purge_and_set("primary_credential", vcred))
}
Err(e) => Err(e),
}
}
None => {
// No credential exists, we can't remove what is not real.
Err(OperationError::InvalidState)
}
}
}
pub(crate) fn check_credential_pw(&self, cleartext: &str) -> Result<bool, OperationError> {
self.primary
.as_ref()
@ -493,6 +415,7 @@ impl Account {
pub(crate) fn existing_credential_id_list(&self) -> Option<Vec<CredentialID>> {
// TODO!!!
// Used in registrations only for disallowing exsiting credentials.
None
}
}
@ -501,6 +424,78 @@ impl Account {
// Need tests for conversion and the cred validations
impl<'a> IdmServerProxyWriteTransaction<'a> {
pub fn service_account_into_person(
&self,
ident: &Identity,
target_uuid: Uuid,
) -> Result<(), OperationError> {
let schema_ref = self.qs_write.get_schema();
// Get the entry.
let account_entry = self
.qs_write
.internal_search_uuid(&target_uuid)
.map_err(|e| {
admin_error!("Failed to start service account into person -> {:?}", e);
e
})?;
// Copy the current classes
let prev_classes: BTreeSet<_> = account_entry
.get_ava_as_iutf8_iter("class")
.ok_or_else(|| {
admin_error!("Invalid entry, class attribute is not present or not iutf8");
OperationError::InvalidAccountState("Missing attribute: class".to_string())
})?
.collect();
// Remove the service account class.
// Add the person class.
let mut new_classes = prev_classes.clone();
new_classes.remove("service_account");
new_classes.insert("person");
// diff the schema attrs, and remove the ones that are service_account only.
let (_added, removed) = schema_ref
.query_attrs_difference(&prev_classes, &new_classes)
.map_err(|se| {
admin_error!("While querying the schema, it reported that requested classes may not be present indicating a possible corruption");
OperationError::SchemaViolation(
se
)
})?;
// Now construct the modlist which:
// removes service_account
let mut modlist =
ModifyList::new_remove("class", PartialValue::new_class("service_account"));
// add person
modlist.push_mod(Modify::Present("class".into(), Value::new_class("person")));
// purge the other attrs that are SA only.
removed
.into_iter()
.for_each(|attr| modlist.push_mod(Modify::Purged(attr.into())));
// purge existing sessions
// Modify
self.qs_write
.impersonate_modify(
// Filter as executed
&filter!(f_eq("uuid", PartialValue::new_uuid(target_uuid))),
// Filter as intended (acp)
&filter_all!(f_eq("uuid", PartialValue::new_uuid(target_uuid))),
&modlist,
// Provide the entry to impersonate
ident,
)
.map_err(|e| {
admin_error!("Failed to migrate service account to person - {:?}", e);
e
})
}
}
#[cfg(test)]
mod tests {
use crate::constants::JSON_ANONYMOUS_V1;

View file

@ -763,7 +763,7 @@ impl AuthSession {
// Do we want to embed this? Or just give the URL? I think we embed
// as we only need the client to be able to check it's not tampered, but
// this isn't a root of trust.
.sign_embed_public_jwk(&uat_jwt_signer)
.sign_embed_public_jwk(uat_jwt_signer)
.map(|jwts| jwts.to_string())
.map_err(|e| {
admin_error!(?e, "Failed to sign UserAuthToken to Jwt");

View file

@ -67,7 +67,7 @@ enum MfaRegState {
TotpInit(Totp),
TotpTryAgain(Totp),
TotpInvalidSha1(Totp, Totp),
Passkey(CreationChallengeResponse, PasskeyRegistration),
Passkey(Box<CreationChallengeResponse>, PasskeyRegistration),
}
impl fmt::Debug for MfaRegState {
@ -208,7 +208,7 @@ impl From<&CredentialUpdateSession> for CredentialUpdateSessionStatus {
),
MfaRegState::TotpTryAgain(_) => MfaRegStateStatus::TotpTryAgain,
MfaRegState::TotpInvalidSha1(_, _) => MfaRegStateStatus::TotpInvalidSha1,
MfaRegState::Passkey(r, _) => MfaRegStateStatus::Passkey(r.clone()),
MfaRegState::Passkey(r, _) => MfaRegStateStatus::Passkey(r.as_ref().clone()),
},
}
}
@ -291,7 +291,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
.qs_write
.get_accesscontrols()
.effective_permission_check(
&ident,
ident,
Some(btreeset![
AttrString::from("primary_credential"),
AttrString::from("passkeys"),
@ -669,7 +669,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
// This shares some common paths between commit and cancel.
fn credential_update_commit_common(
&mut self,
cust: CredentialUpdateSessionToken,
cust: &CredentialUpdateSessionToken,
ct: Duration,
) -> Result<
(
@ -722,7 +722,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
pub fn commit_credential_update(
&mut self,
cust: CredentialUpdateSessionToken,
cust: &CredentialUpdateSessionToken,
ct: Duration,
) -> Result<(), OperationError> {
let (mut modlist, session, session_token) =
@ -818,7 +818,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
pub fn cancel_credential_update(
&mut self,
cust: CredentialUpdateSessionToken,
cust: &CredentialUpdateSessionToken,
ct: Duration,
) -> Result<(), OperationError> {
let (mut modlist, session, session_token) =
@ -1376,7 +1376,7 @@ impl<'a> IdmServerCredUpdateTransaction<'a> {
OperationError::Webauthn
})?;
session.mfaregstate = MfaRegState::Passkey(ccr, pk_reg);
session.mfaregstate = MfaRegState::Passkey(Box::new(ccr), pk_reg);
// Now that it's in the state, it'll be in the status when returned.
Ok(session.deref().into())
}
@ -1386,7 +1386,7 @@ impl<'a> IdmServerCredUpdateTransaction<'a> {
cust: &CredentialUpdateSessionToken,
ct: Duration,
label: String,
reg: RegisterPublicKeyCredential,
reg: &RegisterPublicKeyCredential,
) -> Result<CredentialUpdateSessionStatus, OperationError> {
let session_handle = self.get_current_session(cust, ct)?;
let mut session = session_handle.try_lock().map_err(|_| {
@ -1399,7 +1399,7 @@ impl<'a> IdmServerCredUpdateTransaction<'a> {
MfaRegState::Passkey(_ccr, pk_reg) => {
let passkey = self
.webauthn
.finish_passkey_registration(&reg, pk_reg)
.finish_passkey_registration(reg, pk_reg)
.map_err(|e| {
error!(?e, "Unable to start passkey registration");
OperationError::Webauthn
@ -1506,6 +1506,7 @@ mod tests {
let e1 = entry_init!(
("class", Value::new_class("object")),
("class", Value::new_class("account")),
("class", Value::new_class("service_account")),
("name", Value::new_iname("user_account_only")),
("uuid", Value::new_uuid(testaccount_uuid)),
("description", Value::new_utf8s("testaccount")),
@ -1661,7 +1662,7 @@ mod tests {
let mut idms_prox_write = idms.proxy_write(ct);
idms_prox_write
.commit_credential_update(cust, ct)
.commit_credential_update(&cust, ct)
.expect("Failed to commit credential update.");
idms_prox_write.commit().expect("Failed to commit txn");
@ -2243,6 +2244,7 @@ mod tests {
// There now should be a backup code invalidation present
let da = idms_delayed.try_recv().expect("invalid");
assert!(matches!(da, DelayedAction::BackupCodeRemoval(_)));
let r = task::block_on(idms.delayed_action(ct, da));
assert!(r.is_ok());
@ -2387,7 +2389,7 @@ mod tests {
// Finish the registration
let label = "softtoken".to_string();
let c_status = cutxn
.credential_passkey_finish(&cust, ct, label, passkey_resp)
.credential_passkey_finish(&cust, ct, label, &passkey_resp)
.expect("Failed to initiate passkey registration");
assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));

View file

@ -1,12 +1,14 @@
use crate::prelude::*;
use kanidm_proto::v1::OperationError;
pub struct PasswordChangeEvent {
#[cfg(test)]
pub(crate) struct PasswordChangeEvent {
pub ident: Identity,
pub target: Uuid,
pub cleartext: String,
}
#[cfg(test)]
impl PasswordChangeEvent {
pub fn new_internal(target: &Uuid, cleartext: &str) -> Self {
PasswordChangeEvent {
@ -15,33 +17,6 @@ impl PasswordChangeEvent {
cleartext: cleartext.to_string(),
}
}
pub fn from_idm_account_set_password(
ident: Identity,
cleartext: String,
// qs: &QueryServerWriteTransaction,
) -> Result<Self, OperationError> {
let target = ident.get_uuid().ok_or(OperationError::InvalidState)?;
Ok(PasswordChangeEvent {
ident,
target,
cleartext,
})
}
pub fn from_parts(
// qs: &QueryServerWriteTransaction,
ident: Identity,
target: Uuid,
cleartext: String,
) -> Result<Self, OperationError> {
Ok(PasswordChangeEvent {
ident,
target,
cleartext,
})
}
}
pub struct UnixPasswordChangeEvent {
@ -90,51 +65,6 @@ impl GeneratePasswordEvent {
}
}
#[derive(Debug)]
pub struct GenerateBackupCodeEvent {
pub ident: Identity,
pub target: Uuid,
}
impl GenerateBackupCodeEvent {
pub fn from_parts(
// qs: &QueryServerWriteTransaction,
ident: Identity,
target: Uuid,
) -> Result<Self, OperationError> {
Ok(GenerateBackupCodeEvent { ident, target })
}
#[cfg(test)]
pub fn new_internal(target: Uuid) -> Self {
let ident = Identity::from_internal();
GenerateBackupCodeEvent { ident, target }
}
}
pub struct RemoveBackupCodeEvent {
pub ident: Identity,
pub target: Uuid,
}
impl RemoveBackupCodeEvent {
pub fn from_parts(
// qs: &QueryServerWriteTransaction,
ident: Identity,
target: Uuid,
) -> Result<Self, OperationError> {
Ok(RemoveBackupCodeEvent { ident, target })
}
#[cfg(test)]
pub fn new_internal(target: Uuid) -> Self {
let ident = Identity::from_internal();
RemoveBackupCodeEvent { ident, target }
}
}
#[derive(Debug)]
pub struct RegenerateRadiusSecretEvent {
pub ident: Identity,
@ -265,122 +195,6 @@ impl UnixUserAuthEvent {
}
}
#[derive(Debug)]
pub struct GenerateTotpEvent {
pub ident: Identity,
pub target: Uuid,
}
impl GenerateTotpEvent {
pub fn from_parts(
// qs: &QueryServerWriteTransaction,
ident: Identity,
target: Uuid,
) -> Result<Self, OperationError> {
Ok(GenerateTotpEvent { ident, target })
}
#[cfg(test)]
pub fn new_internal(target: Uuid) -> Self {
let ident = Identity::from_internal();
GenerateTotpEvent { ident, target }
}
}
#[derive(Debug)]
pub struct VerifyTotpEvent {
pub ident: Identity,
pub target: Uuid,
pub session: Uuid,
pub chal: u32,
}
impl VerifyTotpEvent {
pub fn from_parts(
// qs: &QueryServerWriteTransaction,
ident: Identity,
target: Uuid,
session: Uuid,
chal: u32,
) -> Result<Self, OperationError> {
Ok(VerifyTotpEvent {
ident,
target,
session,
chal,
})
}
#[cfg(test)]
pub fn new_internal(target: Uuid, session: Uuid, chal: u32) -> Self {
let ident = Identity::from_internal();
VerifyTotpEvent {
ident,
target,
session,
chal,
}
}
}
#[derive(Debug)]
pub struct AcceptSha1TotpEvent {
pub ident: Identity,
pub target: Uuid,
pub session: Uuid,
}
impl AcceptSha1TotpEvent {
pub fn from_parts(
// qs: &QueryServerWriteTransaction,
ident: Identity,
target: Uuid,
session: Uuid,
) -> Result<Self, OperationError> {
Ok(AcceptSha1TotpEvent {
ident,
target,
session,
})
}
#[cfg(test)]
pub fn new_internal(target: Uuid, session: Uuid) -> Self {
let ident = Identity::from_internal();
AcceptSha1TotpEvent {
ident,
target,
session,
}
}
}
#[derive(Debug)]
pub struct RemoveTotpEvent {
pub ident: Identity,
pub target: Uuid,
}
impl RemoveTotpEvent {
pub fn from_parts(
// qs: &QueryServerWriteTransaction,
ident: Identity,
target: Uuid,
) -> Result<Self, OperationError> {
Ok(RemoveTotpEvent { ident, target })
}
#[cfg(test)]
pub fn new_internal(target: Uuid) -> Self {
let ident = Identity::from_internal();
RemoveTotpEvent { ident, target }
}
}
#[derive(Debug)]
pub struct CredentialStatusEvent {
pub ident: Identity,

View file

@ -1,153 +0,0 @@
use crate::credential::totp::{Totp, TOTP_DEFAULT_STEP};
use crate::identity::IdentityId;
use crate::idm::account::Account;
use kanidm_proto::v1::TotpSecret;
use kanidm_proto::v1::{OperationError, SetCredentialResponse};
use std::mem;
use std::time::Duration;
use uuid::Uuid;
pub(crate) enum MfaRegCred {
Totp(Totp),
}
pub(crate) enum MfaRegNext {
Success,
TotpCheck(TotpSecret),
TotpInvalidSha1,
}
impl MfaRegNext {
#[allow(clippy::wrong_self_convention)]
pub fn to_proto(self, u: Uuid) -> SetCredentialResponse {
match self {
MfaRegNext::Success => SetCredentialResponse::Success,
MfaRegNext::TotpCheck(secret) => SetCredentialResponse::TotpCheck(u, secret),
MfaRegNext::TotpInvalidSha1 => SetCredentialResponse::TotpInvalidSha1(u),
}
}
}
#[derive(Clone)]
enum MfaRegState {
TotpInit(Totp),
TotpInvalidSha1(Totp),
TotpDone,
}
#[derive(Clone)]
pub(crate) struct MfaRegSession {
// The event origin, aka who is requesting the MFA reg (may not
// be the same as account!!!)
origin: IdentityId,
// The account that the MFA will be registered to
pub account: Account,
// What state is the reg process in?
state: MfaRegState,
// Human-facing name of the Domain
issuer: String,
}
impl MfaRegSession {
pub fn totp_new(
origin: IdentityId,
account: Account,
issuer: String,
) -> Result<(Self, MfaRegNext), OperationError> {
// Based on the req, init our session, and the return the next step.
// Store the ID of the event that start's the attempt
let token = Totp::generate_secure(TOTP_DEFAULT_STEP);
let accountname = account.name.as_str();
let next = MfaRegNext::TotpCheck(token.to_proto(accountname, issuer.as_str()));
let state = MfaRegState::TotpInit(token);
let s = MfaRegSession {
origin,
account,
state,
issuer,
};
Ok((s, next))
}
pub fn totp_step(
&mut self,
origin: &IdentityId,
target: &Uuid,
chal: u32,
ct: &Duration,
) -> Result<(MfaRegNext, Option<MfaRegCred>), OperationError> {
if &self.origin != origin || target != &self.account.uuid {
// Verify that the same event source is the one continuing this attempt
return Err(OperationError::InvalidRequestState);
};
match &self.state {
MfaRegState::TotpInit(token) => {
if token.verify(chal, ct) {
let mut nstate = MfaRegState::TotpDone;
mem::swap(&mut self.state, &mut nstate);
match nstate {
MfaRegState::TotpInit(token) => {
Ok((MfaRegNext::Success, Some(MfaRegCred::Totp(token))))
}
_ => Err(OperationError::InvalidState),
}
} else {
// What if it's a broken authenticator app? Google authenticator
// and authy both force sha1 and ignore the algo we send. So let's
// check that just in case.
let token_sha1 = token.clone().downgrade_to_legacy();
if token_sha1.verify(chal, ct) {
// Greeeaaaaaatttt. It's a broken app. Let's check the user
// knows this is broken, before we proceed.
let mut nstate = MfaRegState::TotpInvalidSha1(token_sha1);
mem::swap(&mut self.state, &mut nstate);
Ok((MfaRegNext::TotpInvalidSha1, None))
} else {
// Probably a bad code or typo then. Let them try again.
let accountname = self.account.name.as_str();
Ok((
MfaRegNext::TotpCheck(
token.to_proto(accountname, self.issuer.as_str()),
),
None,
))
}
}
}
_ => Err(OperationError::InvalidRequestState),
}
}
pub fn totp_accept_sha1(
&mut self,
origin: &IdentityId,
target: &Uuid,
) -> Result<(MfaRegNext, Option<MfaRegCred>), OperationError> {
if &self.origin != origin || target != &self.account.uuid {
// Verify that the same event source is the one continuing this attempt
return Err(OperationError::InvalidRequestState);
};
match &self.state {
MfaRegState::TotpInvalidSha1(_token) => {
// They have accepted the token to be sha1, so lets yield that.
// The token was mutated to sha1 in the previous step.
let mut nstate = MfaRegState::TotpDone;
mem::swap(&mut self.state, &mut nstate);
match nstate {
MfaRegState::TotpInvalidSha1(token) => {
Ok((MfaRegNext::Success, Some(MfaRegCred::Totp(token))))
}
_ => Err(OperationError::InvalidState),
}
}
_ => Err(OperationError::InvalidRequestState),
}
}
}

View file

@ -9,7 +9,6 @@ pub(crate) mod credupdatesession;
pub(crate) mod delayed;
pub(crate) mod event;
pub(crate) mod group;
pub(crate) mod mfareg;
pub mod oauth2;
pub(crate) mod radius;
pub mod server;

View file

@ -322,7 +322,7 @@ impl<'a> Oauth2ResourceServersWriteTransaction<'a> {
let scope_maps = ent
.get_ava_as_oauthscopemaps("oauth2_rs_scope_map")
.cloned()
.unwrap_or_else(BTreeMap::new);
.unwrap_or_default();
trace!("implicit_scopes");
let implicit_scopes = ent
@ -385,7 +385,7 @@ impl<'a> Oauth2ResourceServersWriteTransaction<'a> {
let scopes_supported: BTreeSet<String> = implicit_scopes
.iter()
.cloned()
.chain(scope_maps.values().map(|bts| bts.iter()).flatten().cloned())
.chain(scope_maps.values().flat_map(|bts| bts.iter()).cloned())
.collect();
let scopes_supported: Vec<_> = scopes_supported.into_iter().collect();
@ -589,7 +589,7 @@ impl Oauth2ResourceServersReadTransaction {
uat: uat.clone(),
code_challenge,
redirect_uri: auth_req.redirect_uri.clone(),
scopes: avail_scopes.clone(),
scopes: avail_scopes,
nonce: auth_req.nonce.clone(),
};
@ -730,11 +730,14 @@ impl Oauth2ResourceServersReadTransaction {
// Everything is DONE! Now submit that it's all happy and the user consented correctly.
// this will let them bypass consent steps in the future.
// Submit that we consented to the delayed action queue
if let Err(_) = async_tx.send(DelayedAction::Oauth2ConsentGrant(Oauth2ConsentGrant {
target_uuid: uat.uuid,
oauth2_rs_uuid: o2rs.uuid,
scopes: consent_req.scopes,
})) {
if async_tx
.send(DelayedAction::Oauth2ConsentGrant(Oauth2ConsentGrant {
target_uuid: uat.uuid,
oauth2_rs_uuid: o2rs.uuid,
scopes: consent_req.scopes,
}))
.is_err()
{
admin_warn!("unable to queue delayed oauth2 consent grant, continuing ... ");
}
@ -862,7 +865,7 @@ impl Oauth2ResourceServersReadTransaction {
})?;
let mut hasher = sha::Sha256::new();
hasher.update(code_verifier.as_bytes());
let code_verifier_hash: Vec<u8> = hasher.finish().iter().copied().collect();
let code_verifier_hash: Vec<u8> = hasher.finish().to_vec();
if code_challenge.0 != code_verifier_hash {
security_info!(

View file

@ -1,18 +1,17 @@
use crate::credential::policy::CryptoPolicy;
use crate::credential::softlock::CredSoftLock;
use crate::credential::BackupCodes;
use crate::event::{AuthEvent, AuthEventStep, AuthResult};
use crate::identity::{IdentType, IdentUser, Limits};
use crate::idm::account::Account;
use crate::idm::authsession::AuthSession;
use crate::idm::credupdatesession::CredentialUpdateSessionMutex;
#[cfg(test)]
use crate::idm::event::PasswordChangeEvent;
use crate::idm::event::{
AcceptSha1TotpEvent, CredentialStatusEvent, GeneratePasswordEvent, GenerateTotpEvent,
LdapAuthEvent, PasswordChangeEvent, RadiusAuthTokenEvent, RegenerateRadiusSecretEvent,
RemoveTotpEvent, UnixGroupTokenEvent, UnixPasswordChangeEvent, UnixUserAuthEvent,
UnixUserTokenEvent, VerifyTotpEvent,
CredentialStatusEvent, GeneratePasswordEvent, LdapAuthEvent, RadiusAuthTokenEvent,
RegenerateRadiusSecretEvent, UnixGroupTokenEvent, UnixPasswordChangeEvent, UnixUserAuthEvent,
UnixUserTokenEvent,
};
use crate::idm::mfareg::{MfaRegCred, MfaRegNext, MfaRegSession};
use crate::idm::oauth2::{
AccessTokenIntrospectRequest, AccessTokenIntrospectResponse, AccessTokenRequest,
AccessTokenResponse, AuthorisationRequest, AuthorisePermitSuccess, AuthoriseResponse,
@ -24,10 +23,7 @@ use crate::idm::unix::{UnixGroup, UnixUserAccount};
use crate::idm::AuthState;
use crate::ldap::LdapBoundToken;
use crate::prelude::*;
use crate::utils::{
backup_code_from_random, password_from_random, readable_password_from_random,
uuid_from_duration, Sid,
};
use crate::utils::{password_from_random, readable_password_from_random, uuid_from_duration, Sid};
use crate::actors::v1_write::QueryServerWriteV1;
use crate::idm::delayed::{
@ -37,8 +33,8 @@ use crate::idm::delayed::{
use hashbrown::HashSet;
use kanidm_proto::v1::{
AuthType, BackupCodesView, CredentialStatus, PasswordFeedback, RadiusAuthToken,
SetCredentialResponse, UnixGroupToken, UnixUserToken, UserAuthToken,
AuthType, BackupCodesView, CredentialStatus, PasswordFeedback, RadiusAuthToken, UnixGroupToken,
UnixUserToken, UserAuthToken,
};
use compact_jwt::{Jws, JwsSigner, JwsUnverified, JwsValidator};
@ -51,7 +47,9 @@ use tokio::sync::Semaphore;
use async_std::task;
#[cfg(test)]
use core::task::{Context, Poll};
#[cfg(test)]
use futures::task as futures_task;
use concread::{
@ -71,7 +69,7 @@ use url::Url;
use webauthn_rs::prelude::{Webauthn, WebauthnBuilder};
use super::delayed::BackupCodeRemoval;
use super::event::{GenerateBackupCodeEvent, ReadBackupCodeEvent, RemoveBackupCodeEvent};
use super::event::ReadBackupCodeEvent;
use tracing::trace;
@ -86,8 +84,7 @@ pub struct IdmServer {
session_ticket: Semaphore,
sessions: BptreeMap<Uuid, AuthSessionMutex>,
softlocks: HashMap<Uuid, CredSoftLockMutex>,
/// A set of in progress MFA registrations
mfareg_sessions: BptreeMap<Uuid, MfaRegSession>,
/// A set of in progress credential registrations
cred_update_sessions: BptreeMap<Uuid, CredentialUpdateSessionMutex>,
/// Reference to the query server.
qs: QueryServer,
@ -144,7 +141,6 @@ pub struct IdmServerProxyWriteTransaction<'a> {
// qs operations to occur through this interface.
pub qs_write: QueryServerWriteTransaction<'a>,
/// Associate to an event origin ID, which has a TS and a UUID instead
mfareg_sessions: BptreeMapWriteTxn<'a, Uuid, MfaRegSession>,
pub(crate) cred_update_sessions: BptreeMapWriteTxn<'a, Uuid, CredentialUpdateSessionMutex>,
pub(crate) sid: Sid,
crypto_policy: &'a CryptoPolicy,
@ -253,7 +249,6 @@ impl IdmServer {
session_ticket: Semaphore::new(1),
sessions: BptreeMap::new(),
softlocks: HashMap::new(),
mfareg_sessions: BptreeMap::new(),
cred_update_sessions: BptreeMap::new(),
qs,
crypto_policy,
@ -323,7 +318,6 @@ impl IdmServer {
let qs_write = self.qs.write_async(ts).await;
IdmServerProxyWriteTransaction {
mfareg_sessions: self.mfareg_sessions.write(),
cred_update_sessions: self.cred_update_sessions.write(),
qs_write,
sid,
@ -368,6 +362,7 @@ impl IdmServer {
}
impl IdmServerDelayed {
#[cfg(test)]
pub(crate) fn check_is_empty_or_panic(&mut self) {
let waker = futures_task::noop_waker();
let mut cx = Context::from_waker(&waker);
@ -715,7 +710,7 @@ impl<'a> IdmServerAuthTransaction<'a> {
let auth_session_ref = session_read
// Why is the session missing?
.get(&mech.sessionid)
.map(|auth_session_ref| auth_session_ref.clone())
.cloned()
.ok_or_else(|| {
admin_error!("Invalid Session State (no present session uuid)");
OperationError::InvalidSessionState
@ -744,7 +739,7 @@ impl<'a> IdmServerAuthTransaction<'a> {
None => true,
};
let auth_result = if is_valid {
if is_valid {
auth_result
} else {
// Fail the session
@ -757,9 +752,7 @@ impl<'a> IdmServerAuthTransaction<'a> {
state: aus,
delay,
}
});
auth_result
})
} // End AuthEventStep::Mech
AuthEventStep::Cred(creds) => {
// lperf_segment!("idm::server::auth<Creds>", || {
@ -770,7 +763,7 @@ impl<'a> IdmServerAuthTransaction<'a> {
let auth_session_ref = session_read
// Why is the session missing?
.get(&creds.sessionid)
.map(|auth_session_ref| auth_session_ref.clone())
.cloned()
.ok_or_else(|| {
admin_error!("Invalid Session State (no present session uuid)");
OperationError::InvalidSessionState
@ -781,7 +774,7 @@ impl<'a> IdmServerAuthTransaction<'a> {
let maybe_slock_ref = match auth_session.get_credential_uuid()? {
Some(cred_uuid) => {
let softlock_read = self.softlocks.read();
softlock_read.get(&cred_uuid).map(|s| s.clone())
softlock_read.get(&cred_uuid).cloned()
}
None => None,
};
@ -804,7 +797,7 @@ impl<'a> IdmServerAuthTransaction<'a> {
true
};
let r = if is_valid {
if is_valid {
// Process the credentials here as required.
// Basically throw them at the auth_session and see what
// falls out.
@ -843,10 +836,7 @@ impl<'a> IdmServerAuthTransaction<'a> {
state: aus,
delay,
}
});
// softlock_write.commit();
// session_write.commit();
r
})
} // End AuthEventStep::Cred
}
}
@ -1020,7 +1010,7 @@ impl<'a> IdmServerAuthTransaction<'a> {
None
};
let res = if let Some(mut slock) = maybe_valid {
if let Some(mut slock) = maybe_valid {
if account
.verify_unix_credential(lae.cleartext.as_str(), &self.async_tx, ct)?
.is_some()
@ -1067,8 +1057,7 @@ impl<'a> IdmServerAuthTransaction<'a> {
// Account is slocked!
security_info!("Account is softlocked.");
Ok(None)
};
res
}
}
}
@ -1276,15 +1265,6 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
self.webauthn.get_origin()
}
pub fn expire_mfareg_sessions(&mut self, ct: Duration) {
// ct is current time - sub the timeout. and then split.
let expire = ct - Duration::from_secs(MFAREG_SESSION_TIMEOUT);
let split_at = uuid_from_duration(expire, self.sid);
// Removes older sessions in place.
self.mfareg_sessions.split_off_lt(&split_at);
// expired will now be dropped, and can't be used by future sessions.
}
fn check_password_quality(
&mut self,
cleartext: &str,
@ -1368,7 +1348,8 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
}
}
pub fn set_account_password(
#[cfg(test)]
pub(crate) fn set_account_password(
&mut self,
pce: &PasswordChangeEvent,
) -> Result<(), OperationError> {
@ -1588,8 +1569,9 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
})
}
/*
/// Generate a new set of backup code and remove the old ones.
pub fn generate_backup_code(
pub(crate) fn generate_backup_code(
&mut self,
gbe: &GenerateBackupCodeEvent,
) -> Result<Vec<String>, OperationError> {
@ -1626,7 +1608,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
})
}
pub fn remove_backup_code(
pub(crate) fn remove_backup_code(
&mut self,
rte: &RemoveBackupCodeEvent,
) -> Result<SetCredentialResponse, OperationError> {
@ -1653,6 +1635,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
})
.map(|_| SetCredentialResponse::Success)
}
*/
pub fn regenerate_radius_secret(
&mut self,
@ -1691,284 +1674,6 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
.map(|_| cleartext)
}
/*
pub fn reg_account_webauthn_init(
&mut self,
wre: &WebauthnInitRegisterEvent,
ct: Duration,
) -> Result<SetCredentialResponse, OperationError> {
let account = self.target_to_account(&wre.target)?;
let sessionid = uuid_from_duration(ct, self.sid);
let origin = (&wre.ident.origin).into();
let label = wre.label.clone();
let issuer = self.qs_write.get_domain_display_name().to_string();
let (session, mfa_reg_next) =
MfaRegSession::webauthn_new(origin, account, label, self.webauthn, issuer)?;
let next = mfa_reg_next.to_proto(sessionid);
// Add session to tree
self.mfareg_sessions.insert(sessionid, session);
trace!(?sessionid, "Started mfa reg session for webauthn");
Ok(next)
}
pub fn reg_account_webauthn_complete(
&mut self,
wre: &WebauthnDoRegisterEvent,
) -> Result<SetCredentialResponse, OperationError> {
let sessionid = wre.session;
let origin = (&wre.ident.origin).into();
let webauthn = self.webauthn;
// Regardless of the outcome, we purge this session, so we get it
// from the tree instead of a mut pointer.
let mut session = self
.mfareg_sessions
.remove(&sessionid)
.ok_or(OperationError::InvalidState)
.map_err(|e| {
admin_error!("Failed to register webauthn -> {:?}", e);
e
})?;
let (next, wan_cred) = session
.webauthn_step(&origin, &wre.target, &wre.chal, webauthn)
.map_err(|e| {
admin_error!("Failed to register webauthn -> {:?}", e);
OperationError::Webauthn
})?;
if let (MfaRegNext::Success, Some(MfaRegCred::Webauthn(label, cred))) = (&next, wan_cred) {
// Persist the credential
let modlist = session
.account
.gen_securitykey_mod(label, cred)
.map_err(|e| {
admin_error!("Failed to gen webauthn mod {:?}", e);
e
})?;
// Perform the mod
self.qs_write
.impersonate_modify(
// Filter as executed
&filter!(f_eq("uuid", PartialValue::new_uuid(session.account.uuid))),
// Filter as intended (acp)
&filter_all!(f_eq("uuid", PartialValue::new_uuid(session.account.uuid))),
&modlist,
&wre.ident,
)
.map_err(|e| {
admin_error!("reg_account_webauthn_complete {:?}", e);
e
})?;
}
let next = next.to_proto(sessionid);
Ok(next)
}
pub fn remove_account_webauthn(
&mut self,
rwe: &RemoveWebauthnEvent,
) -> Result<SetCredentialResponse, OperationError> {
trace!(
"Attempting to remove webauthn {:?} -> {:?}",
rwe.label,
rwe.target
);
let account = self.target_to_account(&rwe.target)?;
let modlist = account
.gen_securitykey_remove_mod(rwe.label.as_str())
.map_err(|e| {
admin_error!("Failed to gen webauthn remove mod {:?}", e);
e
})?;
// Perform the mod
self.qs_write
.impersonate_modify(
// Filter as executed
&filter!(f_eq("uuid", PartialValue::new_uuid(account.uuid))),
// Filter as intended (acp)
&filter_all!(f_eq("uuid", PartialValue::new_uuid(account.uuid))),
&modlist,
&rwe.ident,
)
.map_err(|e| {
admin_error!("remove_account_webauthn {:?}", e);
e
})
.map(|_| SetCredentialResponse::Success)
}
*/
pub fn generate_account_totp(
&mut self,
gte: &GenerateTotpEvent,
ct: Duration,
) -> Result<SetCredentialResponse, OperationError> {
let account = self.target_to_account(&gte.target)?;
let sessionid = uuid_from_duration(ct, self.sid);
let origin = (&gte.ident.origin).into();
let issuer = self.qs_write.get_domain_display_name().to_string();
let (session, next) = MfaRegSession::totp_new(origin, account, issuer).map_err(|e| {
admin_error!("Unable to start totp MfaRegSession {:?}", e);
e
})?;
let next = next.to_proto(sessionid);
// Add session to tree
self.mfareg_sessions.insert(sessionid, session);
trace!(?sessionid, "Started totp mfa reg session");
Ok(next)
}
pub fn verify_account_totp(
&mut self,
vte: &VerifyTotpEvent,
ct: Duration,
) -> Result<SetCredentialResponse, OperationError> {
let sessionid = vte.session;
let origin = (&vte.ident.origin).into();
let chal = vte.chal;
trace!(?sessionid, "Attempting to find totp mfareg_session");
let (next, opt_cred) = self
.mfareg_sessions
.get_mut(&sessionid)
.ok_or(OperationError::InvalidRequestState)
.and_then(|session| session.totp_step(&origin, &vte.target, chal, &ct))
.map_err(|e| {
admin_error!("Failed to verify totp {:?}", e);
e
})?;
if let (MfaRegNext::Success, Some(MfaRegCred::Totp(token))) = (&next, opt_cred) {
// Purge the session.
let session = self
.mfareg_sessions
.remove(&sessionid)
.ok_or(OperationError::InvalidState)
.map_err(|e| {
admin_error!("Session within totp reg transaction vanished!");
e
})?;
// reg the token
let modlist = session.account.gen_totp_mod(token).map_err(|e| {
admin_error!("Failed to gen totp mod {:?}", e);
e
})?;
// Perform the mod
self.qs_write
.impersonate_modify(
// Filter as executed
&filter!(f_eq("uuid", PartialValue::new_uuid(session.account.uuid))),
// Filter as intended (acp)
&filter_all!(f_eq("uuid", PartialValue::new_uuid(session.account.uuid))),
&modlist,
&vte.ident,
)
.map_err(|e| {
admin_error!("verify_account_totp {:?}", e);
e
})?;
};
let next = next.to_proto(sessionid);
Ok(next)
}
pub fn accept_account_sha1_totp(
&mut self,
aste: &AcceptSha1TotpEvent,
) -> Result<SetCredentialResponse, OperationError> {
let sessionid = aste.session;
let origin = (&aste.ident.origin).into();
trace!(?sessionid, "Attempting to find mfareg_session");
let (next, opt_cred) = self
.mfareg_sessions
.get_mut(&sessionid)
.ok_or(OperationError::InvalidRequestState)
.and_then(|session| session.totp_accept_sha1(&origin, &aste.target))
.map_err(|e| {
admin_error!("Failed to accept SHA1 TOTP {:?}", e);
e
})?;
if let (MfaRegNext::Success, Some(MfaRegCred::Totp(token))) = (&next, opt_cred) {
// Purge the session.
let session = self
.mfareg_sessions
.remove(&sessionid)
.ok_or(OperationError::InvalidState)
.map_err(|e| {
admin_error!("Session within transaction vanished!");
e
})?;
// reg the token
let modlist = session.account.gen_totp_mod(token).map_err(|e| {
admin_error!("Failed to gen TOTP mod {:?}", e);
e
})?;
// Perform the mod
self.qs_write
.impersonate_modify(
// Filter as executed
&filter!(f_eq("uuid", PartialValue::new_uuid(session.account.uuid))),
// Filter as intended (acp)
&filter_all!(f_eq("uuid", PartialValue::new_uuid(session.account.uuid))),
&modlist,
&aste.ident,
)
.map_err(|e| {
admin_error!("accept_account_sha1_totp {:?}", e);
e
})?;
};
let next = next.to_proto(sessionid);
Ok(next)
}
pub fn remove_account_totp(
&mut self,
rte: &RemoveTotpEvent,
) -> Result<SetCredentialResponse, OperationError> {
trace!(target = ?rte.target, "Attempting to remove totp");
let account = self.target_to_account(&rte.target)?;
let modlist = account.gen_totp_remove_mod().map_err(|e| {
admin_error!("Failed to gen totp remove mod {:?}", e);
e
})?;
// Perform the mod
self.qs_write
.impersonate_modify(
// Filter as executed
&filter!(f_eq("uuid", PartialValue::new_uuid(account.uuid))),
// Filter as intended (acp)
&filter_all!(f_eq("uuid", PartialValue::new_uuid(account.uuid))),
&modlist,
&rte.ident,
)
.map_err(|e| {
admin_error!("remove_account_totp {:?}", e);
e
})
.map(|_| SetCredentialResponse::Success)
}
// -- delayed action processing --
fn process_pwupgrade(&mut self, pwu: &PasswordUpgrade) -> Result<(), OperationError> {
// get the account
@ -2161,7 +1866,6 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
self.uat_jwt_validator.commit();
self.token_enc_key.commit();
self.pw_badlist_cache.commit();
self.mfareg_sessions.commit();
self.cred_update_sessions.commit();
trace!("cred_update_session.commit");
self.qs_write.commit()
@ -2184,20 +1888,16 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
#[cfg(test)]
mod tests {
use crate::credential::policy::CryptoPolicy;
use crate::credential::totp::Totp;
use crate::credential::{Credential, Password};
use crate::event::{AuthEvent, AuthResult, CreateEvent, ModifyEvent};
use crate::idm::delayed::{BackupCodeRemoval, DelayedAction};
use crate::idm::event::{
AcceptSha1TotpEvent, GenerateBackupCodeEvent, GenerateTotpEvent, PasswordChangeEvent,
RadiusAuthTokenEvent, RegenerateRadiusSecretEvent, RemoveTotpEvent, UnixGroupTokenEvent,
UnixPasswordChangeEvent, UnixUserAuthEvent, UnixUserTokenEvent, VerifyTotpEvent,
PasswordChangeEvent, RadiusAuthTokenEvent, RegenerateRadiusSecretEvent,
UnixGroupTokenEvent, UnixPasswordChangeEvent, UnixUserAuthEvent, UnixUserTokenEvent,
};
use crate::idm::AuthState;
use crate::modify::{Modify, ModifyList};
use crate::prelude::*;
use kanidm_proto::v1::OperationError;
use kanidm_proto::v1::SetCredentialResponse;
use kanidm_proto::v1::{AuthAllowed, AuthMech, AuthType};
use crate::idm::server::{IdmServer, IdmServerTransaction};
@ -2931,194 +2631,6 @@ mod tests {
)
}
#[test]
fn test_idm_totp_registration() {
run_idm_test!(
|_qs: &QueryServer, idms: &IdmServer, _idms_delayed: &IdmServerDelayed| {
let ct = duration_from_epoch_now();
let expire = Duration::from_secs(ct.as_secs() + MFAREG_SESSION_TIMEOUT + 2);
let mut idms_prox_write = idms.proxy_write(ct.clone());
// verify with no session (fail)
let vte1 = VerifyTotpEvent::new_internal(UUID_ADMIN.clone(), Uuid::new_v4(), 0);
match idms_prox_write.verify_account_totp(&vte1, ct.clone()) {
Err(e) => {
assert!(e == OperationError::InvalidRequestState);
}
_ => panic!(),
};
// reg, expire session, attempt verify (fail)
let gte1 = GenerateTotpEvent::new_internal(UUID_ADMIN.clone());
let res = idms_prox_write
.generate_account_totp(&gte1, ct.clone())
.unwrap();
let sesid = match res {
SetCredentialResponse::TotpCheck(id, _) => id,
_ => panic!("invalid state!"),
};
idms_prox_write.expire_mfareg_sessions(expire.clone());
let vte2 = VerifyTotpEvent::new_internal(UUID_ADMIN.clone(), sesid, 0);
match idms_prox_write.verify_account_totp(&vte1, ct.clone()) {
Err(e) => {
assert!(e == OperationError::InvalidRequestState);
}
_ => panic!(),
};
// == Test TOTP on account with no password (fail)
let res = idms_prox_write
.generate_account_totp(&gte1, ct.clone())
.unwrap();
let (sesid, tok) = match res {
SetCredentialResponse::TotpCheck(id, tok) => (id, tok),
_ => panic!("invalid state!"),
};
// get the correct otp
let r_tok: Totp = tok.into();
let chal = r_tok
.do_totp_duration_from_epoch(&ct)
.expect("Failed to do totp?");
// attempt the verify
let vte3 = VerifyTotpEvent::new_internal(UUID_ADMIN.clone(), sesid, chal);
match idms_prox_write.verify_account_totp(&vte3, ct.clone()) {
Err(e) => assert!(e == OperationError::InvalidState),
_ => panic!(),
};
// Expire the session to allow it to reset.
idms_prox_write.expire_mfareg_sessions(expire.clone());
// Set a password.
let pce = PasswordChangeEvent::new_internal(&UUID_ADMIN, TEST_PASSWORD);
assert!(idms_prox_write.set_account_password(&pce).is_ok());
// == reg, but change the event source part way in the process (failure)
let res = idms_prox_write
.generate_account_totp(&gte1, ct.clone())
.unwrap();
let (sesid, tok) = match res {
SetCredentialResponse::TotpCheck(id, tok) => (id, tok),
_ => panic!("invalid state!"),
};
// get the correct otp
let r_tok: Totp = tok.into();
let chal = r_tok
.do_totp_duration_from_epoch(&ct)
.expect("Failed to do totp?");
// attempt the verify
let vte3 = VerifyTotpEvent::new_internal(UUID_ANONYMOUS.clone(), sesid, chal);
match idms_prox_write.verify_account_totp(&vte3, ct.clone()) {
Err(e) => assert!(e == OperationError::InvalidRequestState),
_ => panic!(),
};
// == reg, verify w_ incorrect totp (fail)
let res = idms_prox_write
.generate_account_totp(&gte1, ct.clone())
.unwrap();
let (_sesid, _tok) = match res {
SetCredentialResponse::TotpCheck(id, tok) => (id, tok),
_ => panic!("invalid state!"),
};
// We can reuse the OTP/Vte2 from before, since we want the invalid otp.
match idms_prox_write.verify_account_totp(&vte2, ct.clone()) {
// On failure we get back another attempt to setup the token.
Ok(SetCredentialResponse::TotpCheck(_id, _tok)) => {}
_ => panic!(),
};
idms_prox_write.expire_mfareg_sessions(expire.clone());
// Turn the pts into an otp
// == reg, verify w_ correct totp (success)
let res = idms_prox_write
.generate_account_totp(&gte1, ct.clone())
.unwrap();
let (sesid, tok) = match res {
SetCredentialResponse::TotpCheck(id, tok) => (id, tok),
_ => panic!("invalid state!"),
};
// We can't reuse the OTP/Vte from before, since the token seed changes
let r_tok: Totp = tok.into();
let chal = r_tok
.do_totp_duration_from_epoch(&ct)
.expect("Failed to do totp?");
// attempt the verify
let vte3 = VerifyTotpEvent::new_internal(UUID_ADMIN.clone(), sesid, chal);
match idms_prox_write.verify_account_totp(&vte3, ct.clone()) {
Ok(SetCredentialResponse::Success) => {}
_ => panic!(),
};
idms_prox_write.expire_mfareg_sessions(expire.clone());
// Test removing the TOTP and then authing with password only.
let rte = RemoveTotpEvent::new_internal(UUID_ADMIN.clone());
idms_prox_write.remove_account_totp(&rte).unwrap();
assert!(idms_prox_write.commit().is_ok());
check_admin_password(idms, TEST_PASSWORD);
// All done!
}
)
}
#[test]
fn test_idm_totp_sha1_registration() {
run_idm_test!(
|_qs: &QueryServer, idms: &IdmServer, _idms_delayed: &IdmServerDelayed| {
let ct = duration_from_epoch_now();
let mut idms_prox_write = idms.proxy_write(ct.clone());
let pce = PasswordChangeEvent::new_internal(&UUID_ADMIN, TEST_PASSWORD);
assert!(idms_prox_write.set_account_password(&pce).is_ok());
// Start registering the TOTP
let gte1 = GenerateTotpEvent::new_internal(UUID_ADMIN.clone());
let res = idms_prox_write
.generate_account_totp(&gte1, ct.clone())
.unwrap();
let (sesid, tok) = match res {
SetCredentialResponse::TotpCheck(id, tok) => (id, tok),
_ => panic!("invalid state!"),
};
let r_tok: Totp = tok.into();
// Now, assert that the Totp is NOT sha1 (correct default behaviour).
assert!(!r_tok.is_legacy_algo());
// Mutate the tok to a legacy token.
let legacy_tok = r_tok.downgrade_to_legacy();
let chal = legacy_tok
.do_totp_duration_from_epoch(&ct)
.expect("Failed to do totp?");
// attempt the verify
let vte3 = VerifyTotpEvent::new_internal(UUID_ADMIN.clone(), sesid, chal);
match idms_prox_write.verify_account_totp(&vte3, ct.clone()) {
Ok(SetCredentialResponse::TotpInvalidSha1(_)) => {}
_ => panic!(),
};
let aste = AcceptSha1TotpEvent::new_internal(UUID_ADMIN.clone(), sesid);
match idms_prox_write.accept_account_sha1_totp(&aste) {
Ok(SetCredentialResponse::Success) => {}
_ => panic!(),
};
// Done!
}
)
}
#[test]
fn test_idm_simple_password_upgrade() {
run_idm_test!(
@ -3722,85 +3234,6 @@ mod tests {
)
}
#[test]
fn test_idm_backup_code_removal_delayed_action() {
run_idm_test!(
|_qs: &QueryServer, idms: &IdmServer, idms_delayed: &mut IdmServerDelayed| {
let ct = duration_from_epoch_now();
let expire = ct - Duration::from_secs(AUTH_SESSION_TIMEOUT);
let mut idms_prox_write = idms.proxy_write(ct.clone());
// let mut wa_softtok = WebauthnAuthenticator::new(SoftPasskey::new());
// The account must has primary credential + uses MFA before generating backup codes
// Set a password.
let pce = PasswordChangeEvent::new_internal(&UUID_ADMIN, TEST_PASSWORD);
assert!(idms_prox_write.set_account_password(&pce).is_ok());
// Generate TOTP
let gte1 = GenerateTotpEvent::new_internal(UUID_ADMIN.clone());
// == reg, verify w_ correct totp (success)
let res = idms_prox_write
.generate_account_totp(&gte1, ct.clone())
.unwrap();
let (sesid, tok) = match res {
SetCredentialResponse::TotpCheck(id, tok) => (id, tok),
_ => panic!("invalid state!"),
};
let r_tok: Totp = tok.into();
let chal = r_tok
.do_totp_duration_from_epoch(&ct)
.expect("Failed to do totp?");
// attempt the verify
let vte3 = VerifyTotpEvent::new_internal(UUID_ADMIN.clone(), sesid, chal);
match idms_prox_write.verify_account_totp(&vte3, ct.clone()) {
Ok(SetCredentialResponse::Success) => {}
_ => panic!(),
};
idms_prox_write.expire_mfareg_sessions(expire.clone());
// Generate backup codes
let gbe = GenerateBackupCodeEvent::new_internal(UUID_ADMIN.clone());
let backup_codes_vec = idms_prox_write.generate_backup_code(&gbe).unwrap();
let code_for_test = &backup_codes_vec[0];
// Get the account now so we can peek at the registered credential.
let account = idms_prox_write
.target_to_account(&UUID_ADMIN)
.expect("account must exist");
let cred = account.primary.expect("Must exist.");
let backup_codes_view = cred.get_backup_code_view().expect("must have view");
assert!(backup_codes_view.backup_codes.contains(code_for_test));
assert!(idms_prox_write.commit().is_ok());
// Assert the delayed action queue is empty
idms_delayed.check_is_empty_or_panic();
// Generate a fake action to remove one backup code
let da = DelayedAction::BackupCodeRemoval(BackupCodeRemoval {
target_uuid: UUID_ADMIN.clone(),
code_to_remove: code_for_test.to_string(),
});
let r = task::block_on(idms.delayed_action(duration_from_epoch_now(), da));
assert!(Ok(true) == r);
// Check the removed backup code is no longer in the set
let mut idms_prox_write = idms.proxy_write(ct.clone());
let account = idms_prox_write
.target_to_account(&UUID_ADMIN)
.expect("account must exist");
let cred = account.primary.expect("Must exist.");
let backup_codes_view = cred.get_backup_code_view().expect("must have view");
assert!(!backup_codes_view.backup_codes.contains(code_for_test));
}
)
}
#[test]
fn test_idm_jwt_uat_expiry() {
run_idm_test!(
@ -3980,4 +3413,39 @@ mod tests {
}
)
}
#[test]
fn test_idm_service_account_to_person() {
run_idm_test!(|_qs: &QueryServer,
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed| {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let idms_prox_write = idms.proxy_write(ct.clone());
let ident = Identity::from_internal();
let target_uuid = Uuid::new_v4();
// Create a service account
let e = entry_init!(
("class", Value::new_class("object")),
("class", Value::new_class("account")),
("class", Value::new_class("service_account")),
("name", Value::new_iname("testaccount")),
("uuid", Value::new_uuid(target_uuid)),
("description", Value::new_utf8s("testaccount")),
("displayname", Value::new_utf8s("Test Account"))
);
let ce = CreateEvent::new_internal(vec![e]);
let cr = idms_prox_write.qs_write.create(&ce);
assert!(cr.is_ok());
// Do the migrate.
assert!(idms_prox_write
.service_account_into_person(&ident, target_uuid)
.is_ok());
// Any checks?
})
}
}

View file

@ -97,13 +97,16 @@ impl IntervalActor {
);
sleep(Duration::from_secs(wait_seconds)).await;
server
if let Err(e) = server
.handle_online_backup(
OnlineBackupEvent::new(),
outpath.clone().as_str(),
versions,
)
.await;
.await
{
error!(?e, "An online backup error occured.");
}
}
});

View file

@ -848,4 +848,34 @@ mod tests {
}
)
}
#[test]
fn test_ldap_token_privilege_granting() {
run_idm_test!(
|_qs: &QueryServer, idms: &IdmServer, _idms_delayed: &IdmServerDelayed| {
// Configure the user account that will have the tokens issued.
// Should be a SERVICE account.
// Should I finally do the class rules shit?
// Issue a token, make it purpose = ldap.
// assert the uat fails on non-ldap events.
// Setup a person with
// Setup an access control for the service account to view mail attrs.
// Setup the ldap server
let _ldaps = LdapServer::new(idms).expect("failed to start ldap");
// Bind with account pw, search and show attr isn't accessible.
// Bind using the token instead of the PW.
// Search and retrieve an attribute that's now accessible.
assert!(true);
}
)
}
}

View file

@ -1,3 +1,4 @@
#[cfg(test)]
macro_rules! setup_test {
() => {{
let _ = sketching::test_init();
@ -125,6 +126,7 @@ macro_rules! entry_str_to_account {
}};
}
#[cfg(test)]
macro_rules! run_idm_test_inner {
($test_fn:expr) => {{
#[allow(unused_imports)]
@ -166,6 +168,7 @@ macro_rules! run_idm_test {
}};
}
#[cfg(test)]
pub fn run_idm_test_no_logging<F>(mut test_fn: F)
where
F: FnMut(

View file

@ -173,7 +173,7 @@ mod tests {
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
r#"{
"attrs": {
"class": ["account"],
"class": ["account", "service_account"],
"name": ["testperson"],
"uuid": ["83a0927f-3de1-45ec-bea0-2f7b997ef244"],
"description": ["testperson"],

View file

@ -78,7 +78,7 @@ fn do_memberof(
} else {
// Means MO is empty, so we need to duplicate dmo to allow things to
// proceed.
mo = Some(dmo.clone());
mo = Some(dmo);
};
};

View file

@ -91,14 +91,11 @@ impl Plugin for ReferentialIntegrity {
let ref_types = schema.get_reference_types();
// Fast Path
let mut vsiter = cand
.iter()
.map(|c| {
ref_types
.values()
.filter_map(move |rtype| c.get_ava_set(&rtype.name))
})
.flatten();
let mut vsiter = cand.iter().flat_map(|c| {
ref_types
.values()
.filter_map(move |rtype| c.get_ava_set(&rtype.name))
});
// Could check len first?
let mut i = Vec::new();
@ -183,11 +180,10 @@ impl Plugin for ReferentialIntegrity {
// .iter()
cand.iter()
.map(|e| e.get_uuid())
.map(|u| ref_types.values().map(move |r_type| {
.flat_map(|u| ref_types.values().map(move |r_type| {
// For everything that references the uuid's in the deleted set.
f_eq(r_type.name.as_str(), PartialValue::new_refer(u))
}))
.flatten()
.collect(),
));

View file

@ -203,7 +203,7 @@ mod tests {
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
r#"{
"attrs": {
"class": ["account"],
"class": ["account", "service_account"],
"name": ["testperson"],
"description": ["testperson"],
"displayname": ["testperson"]
@ -230,7 +230,7 @@ mod tests {
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
r#"{
"attrs": {
"class": ["account"],
"class": ["account", "service_account"],
"name": ["testperson"],
"description": ["testperson"],
"displayname": ["testperson"]
@ -257,7 +257,7 @@ mod tests {
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
r#"{
"attrs": {
"class": ["account"],
"class": ["account", "service_account"],
"spn": ["testperson@invalid_domain.com"],
"name": ["testperson"],
"description": ["testperson"],
@ -284,7 +284,7 @@ mod tests {
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
r#"{
"attrs": {
"class": ["account"],
"class": ["account", "service_account"],
"name": ["testperson"],
"description": ["testperson"],
"displayname": ["testperson"]

View file

@ -70,8 +70,8 @@ impl fmt::Display for State {
enum Transition {
Create(Eattrs),
ModifyPurge(AttrString),
ModifyPresent(AttrString, Value),
ModifyRemoved(AttrString, PartialValue),
ModifyPresent(AttrString, Box<Value>),
ModifyRemoved(AttrString, Box<PartialValue>),
Recycle,
Revive,
Tombstone(Eattrs),
@ -107,11 +107,11 @@ impl State {
(State::Live(ref mut attrs), Transition::ModifyPresent(attr, value)) => {
trace!("Live + ModifyPresent({}) -> Live", attr);
if let Some(vs) = attrs.get_mut(attr) {
let r = vs.insert_checked(value.clone());
let r = vs.insert_checked(value.as_ref().clone());
assert!(r.is_ok());
// Reject if it fails?
} else {
let vs = valueset::from_value_iter(std::iter::once(value.clone()))
let vs = valueset::from_value_iter(std::iter::once(value.as_ref().clone()))
.expect("Unable to fail - not empty, and only one type!");
attrs.insert(attr.clone(), vs);
}
@ -218,19 +218,13 @@ impl EntryChangelog {
.map(|c| c.contains(&PVCLASS_TOMBSTONE as &PartialValue))
.unwrap_or(false)
{
(
btreemap![(cid.clone(), State::Tombstone(attrs))],
BTreeMap::new(),
)
(btreemap![(cid, State::Tombstone(attrs))], BTreeMap::new())
} else if class
.as_ref()
.map(|c| c.contains(&PVCLASS_RECYCLED as &PartialValue))
.unwrap_or(false)
{
(
btreemap![(cid.clone(), State::Recycled(attrs))],
BTreeMap::new(),
)
(btreemap![(cid, State::Recycled(attrs))], BTreeMap::new())
} else {
(
btreemap![(cid.clone(), State::NonExistent)],
@ -262,7 +256,7 @@ impl EntryChangelog {
viter
.into_iter()
.map(|v| Transition::ModifyPresent(AttrString::from(attr), v))
.map(|v| Transition::ModifyPresent(AttrString::from(attr), Box::new(v)))
.for_each(|t| change.s.push(t));
}
@ -282,7 +276,7 @@ impl EntryChangelog {
viter
.into_iter()
.map(|v| Transition::ModifyRemoved(AttrString::from(attr), v))
.map(|v| Transition::ModifyRemoved(AttrString::from(attr), Box::new(v)))
.for_each(|t| change.s.push(t));
}
@ -510,7 +504,7 @@ impl EntryChangelog {
// insert_anchor will remove anything to the right, we also need to
// remove everything to the left, so just clear.
let _ = self.anchors.clear();
self.anchors.clear();
self.anchors.insert(cid.clone(), entry_state);
// And now split the CL.

View file

@ -3,6 +3,7 @@ use crate::repl::cid::Cid;
use concread::bptree::{BptreeMap, BptreeMapReadTxn, BptreeMapWriteTxn};
use idlset::v2::IDLBitRange;
use kanidm_proto::v1::ConsistencyError;
use std::cmp::Ordering;
use std::collections::BTreeMap;
use std::ops::Bound::*;
use std::sync::Arc;
@ -60,7 +61,7 @@ pub trait ReplicationUpdateVectorTransaction {
// We don't need the details of the change - only the cid of the
// change that this entry was involved in.
for cid in eclog.cid_iter() {
if let Some(idl) = check_ruv.get_mut(&cid) {
if let Some(idl) = check_ruv.get_mut(cid) {
// We can't guarantee id order, so we have to do this properly.
idl.insert_id(eid);
} else {
@ -86,26 +87,30 @@ pub trait ReplicationUpdateVectorTransaction {
let mut snap_next = snap_iter.next();
while let (Some((ck, cv)), Some((sk, sv))) = (&check_next, &snap_next) {
if ck == sk {
if cv == sv {
trace!("{:?} is consistent!", ck);
} else {
admin_warn!("{:?} is NOT consistent! IDL's differ", ck);
match ck.cmp(sk) {
Ordering::Equal => {
if cv == sv {
trace!("{:?} is consistent!", ck);
} else {
admin_warn!("{:?} is NOT consistent! IDL's differ", ck);
debug_assert!(false);
results.push(Err(ConsistencyError::RuvInconsistent(ck.to_string())));
}
check_next = check_iter.next();
snap_next = snap_iter.next();
}
Ordering::Less => {
admin_warn!("{:?} is NOT consistent! CID missing from RUV", ck);
debug_assert!(false);
results.push(Err(ConsistencyError::RuvInconsistent(ck.to_string())));
check_next = check_iter.next();
}
Ordering::Greater => {
admin_warn!("{:?} is NOT consistent! CID should not exist in RUV", sk);
debug_assert!(false);
results.push(Err(ConsistencyError::RuvInconsistent(sk.to_string())));
snap_next = snap_iter.next();
}
check_next = check_iter.next();
snap_next = snap_iter.next();
} else if ck < sk {
admin_warn!("{:?} is NOT consistent! CID missing from RUV", ck);
debug_assert!(false);
results.push(Err(ConsistencyError::RuvInconsistent(ck.to_string())));
check_next = check_iter.next();
} else {
admin_warn!("{:?} is NOT consistent! CID should not exist in RUV", sk);
debug_assert!(false);
results.push(Err(ConsistencyError::RuvInconsistent(sk.to_string())));
snap_next = snap_iter.next();
}
}
@ -159,7 +164,7 @@ impl<'a> ReplicationUpdateVectorWriteTransaction<'a> {
// We don't need the details of the change - only the cid of the
// change that this entry was involved in.
for cid in eclog.cid_iter() {
if let Some(idl) = rebuild_ruv.get_mut(&cid) {
if let Some(idl) = rebuild_ruv.get_mut(cid) {
// We can't guarantee id order, so we have to do this properly.
idl.insert_id(eid);
} else {
@ -181,9 +186,9 @@ impl<'a> ReplicationUpdateVectorWriteTransaction<'a> {
Ok(())
}
pub fn insert_change(&mut self, cid: Cid, idl: IDLBitRange) -> Result<(), OperationError> {
pub fn insert_change(&mut self, cid: &Cid, idl: IDLBitRange) -> Result<(), OperationError> {
// Remember, in a transaction the changes can be updated multiple times.
if let Some(ex_idl) = self.data.get_mut(&cid) {
if let Some(ex_idl) = self.data.get_mut(cid) {
// This ensures both sets have all the available ids.
let idl = ex_idl as &_ | &idl;
*ex_idl = idl;

View file

@ -23,6 +23,7 @@ use kanidm_proto::v1::{ConsistencyError, OperationError, SchemaError};
use tracing::trace;
use hashbrown::{HashMap, HashSet};
use std::collections::BTreeSet;
use uuid::Uuid;
use concread::cowcell::*;
@ -292,18 +293,27 @@ impl SchemaAttribute {
///
/// [`Entry`]: ../entry/index.html
/// [`access`]: ../access/index.html
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Default)]
pub struct SchemaClass {
// Is this used?
// class: Vec<String>,
pub name: AttrString,
pub uuid: Uuid,
pub description: String,
// This allows modification of system types to be extended in custom ways
/// This allows modification of system types to be extended in custom ways
pub systemmay: Vec<AttrString>,
pub may: Vec<AttrString>,
pub systemmust: Vec<AttrString>,
pub must: Vec<AttrString>,
/// A list of classes that this extends. These are an "or", as at least one
/// of the supplementing classes must also be present. Think of this as
/// "inherits toward" or "provides". This is just as "strict" as requires but
/// operates in the opposite direction allowing a tree structure.
pub systemsupplements: Vec<AttrString>,
pub supplements: Vec<AttrString>,
/// A list of classes that can not co-exist with this item at the same time.
pub systemexcludes: Vec<AttrString>,
pub excludes: Vec<AttrString>,
}
impl SchemaClass {
@ -354,6 +364,23 @@ impl SchemaClass {
.map(|i| i.map(AttrString::from).collect())
.unwrap_or_else(Vec::new);
let systemsupplements = value
.get_ava_iter_iutf8("systemsupplements")
.map(|i| i.map(AttrString::from).collect())
.unwrap_or_else(Vec::new);
let supplements = value
.get_ava_iter_iutf8("supplements")
.map(|i| i.map(AttrString::from).collect())
.unwrap_or_else(Vec::new);
let systemexcludes = value
.get_ava_iter_iutf8("systemexcludes")
.map(|i| i.map(AttrString::from).collect())
.unwrap_or_else(Vec::new);
let excludes = value
.get_ava_iter_iutf8("excludes")
.map(|i| i.map(AttrString::from).collect())
.unwrap_or_else(Vec::new);
Ok(SchemaClass {
name,
uuid,
@ -362,8 +389,22 @@ impl SchemaClass {
may,
systemmust,
must,
systemsupplements,
supplements,
systemexcludes,
excludes,
})
}
/// An iterator over the full set of attrs that may or must exist
/// on this class.
pub fn may_iter(&self) -> impl Iterator<Item = &AttrString> {
self.systemmay
.iter()
.chain(self.may.iter())
.chain(self.systemmust.iter())
.chain(self.must.iter())
}
}
pub trait SchemaTransaction {
@ -449,6 +490,59 @@ pub trait SchemaTransaction {
None
}
}
fn query_attrs_difference(
&self,
prev_class: &BTreeSet<&str>,
new_class: &BTreeSet<&str>,
) -> Result<(BTreeSet<&str>, BTreeSet<&str>), SchemaError> {
let schema_classes = self.get_classes();
let mut invalid_classes = Vec::with_capacity(0);
let prev_attrs: BTreeSet<&str> = prev_class
.iter()
.filter_map(|cls| match schema_classes.get(*cls) {
Some(x) => Some(x.may_iter()),
None => {
admin_debug!("invalid class: {:?}", cls);
invalid_classes.push(cls.to_string());
None
}
})
// flatten all the inner iters.
.flatten()
.map(|s| s.as_str())
.collect();
if !invalid_classes.is_empty() {
return Err(SchemaError::InvalidClass(invalid_classes));
};
let new_attrs: BTreeSet<&str> = new_class
.iter()
.filter_map(|cls| match schema_classes.get(*cls) {
Some(x) => Some(x.may_iter()),
None => {
admin_debug!("invalid class: {:?}", cls);
invalid_classes.push(cls.to_string());
None
}
})
// flatten all the inner iters.
.flatten()
.map(|s| s.as_str())
.collect();
if !invalid_classes.is_empty() {
return Err(SchemaError::InvalidClass(invalid_classes));
};
let removed = prev_attrs.difference(&new_attrs).copied().collect();
let added = new_attrs.difference(&prev_attrs).copied().collect();
Ok((added, removed))
}
}
impl<'a> SchemaWriteTransaction<'a> {
@ -531,7 +625,7 @@ impl<'a> SchemaWriteTransaction<'a> {
.flat_map(|a| {
a.index.iter().map(move |itype: &IndexType| IdxKey {
attr: a.name.clone(),
itype: (*itype).clone(),
itype: *itype,
})
})
.collect()
@ -774,6 +868,67 @@ impl<'a> SchemaWriteTransaction<'a> {
syntax: SyntaxType::Utf8StringInsensitive,
},
);
self.attributes.insert(
AttrString::from("systemsupplements"),
SchemaAttribute {
name: AttrString::from("systemsupplements"),
uuid: UUID_SCHEMA_ATTR_SYSTEMSUPPLEMENTS,
description: String::from(
"A set of classes that this type supplements too, where this class can't exist without their presence.",
),
multivalue: true,
unique: false,
phantom: false,
index: vec![],
syntax: SyntaxType::Utf8StringInsensitive,
},
);
self.attributes.insert(
AttrString::from("supplements"),
SchemaAttribute {
name: AttrString::from("supplements"),
uuid: UUID_SCHEMA_ATTR_SUPPLEMENTS,
description: String::from(
"A set of user modifiable classes, where this determines that at least one other type must supplement this type",
),
multivalue: true,
unique: false,
phantom: false,
index: vec![],
syntax: SyntaxType::Utf8StringInsensitive,
},
);
self.attributes.insert(
AttrString::from("systemexcludes"),
SchemaAttribute {
name: AttrString::from("systemexcludes"),
uuid: UUID_SCHEMA_ATTR_SYSTEMEXCLUDES,
description: String::from(
"A set of classes that are denied presence in connection to this class",
),
multivalue: true,
unique: false,
phantom: false,
index: vec![],
syntax: SyntaxType::Utf8StringInsensitive,
},
);
self.attributes.insert(
AttrString::from("excludes"),
SchemaAttribute {
name: AttrString::from("excludes"),
uuid: UUID_SCHEMA_ATTR_EXCLUDES,
description: String::from(
"A set of user modifiable classes that are denied presence in connection to this class",
),
multivalue: true,
unique: false,
phantom: false,
index: vec![],
syntax: SyntaxType::Utf8StringInsensitive,
},
);
// SYSINFO attrs
// ACP attributes.
self.attributes.insert(
@ -988,6 +1143,22 @@ impl<'a> SchemaWriteTransaction<'a> {
syntax: SyntaxType::Utf8StringInsensitive,
},
);
self.attributes.insert(
AttrString::from("scope"),
SchemaAttribute {
name: AttrString::from("scope"),
uuid: UUID_SCHEMA_ATTR_SCOPE,
description: String::from(
"The string identifier of a permission scope in a session",
),
multivalue: true,
unique: false,
phantom: true,
index: vec![],
syntax: SyntaxType::Utf8StringInsensitive,
},
);
self.attributes.insert(
AttrString::from("password_import"),
SchemaAttribute {
@ -1142,7 +1313,6 @@ impl<'a> SchemaWriteTransaction<'a> {
uuid: UUID_SCHEMA_CLASS_ATTRIBUTETYPE,
description: String::from("Definition of a schema attribute"),
systemmay: vec![AttrString::from("phantom"), AttrString::from("index")],
may: vec![],
systemmust: vec![
AttrString::from("class"),
AttrString::from("attributename"),
@ -1151,7 +1321,8 @@ impl<'a> SchemaWriteTransaction<'a> {
AttrString::from("syntax"),
AttrString::from("description"),
],
must: vec![],
systemexcludes: vec![AttrString::from("classtype")],
..Default::default()
},
);
self.classes.insert(
@ -1165,14 +1336,18 @@ impl<'a> SchemaWriteTransaction<'a> {
AttrString::from("may"),
AttrString::from("systemmust"),
AttrString::from("must"),
AttrString::from("systemsupplements"),
AttrString::from("supplements"),
AttrString::from("systemexcludes"),
AttrString::from("excludes"),
],
may: vec![],
systemmust: vec![
AttrString::from("class"),
AttrString::from("classname"),
AttrString::from("description"),
],
must: vec![],
systemexcludes: vec![AttrString::from("attributetype")],
..Default::default()
},
);
self.classes.insert(
@ -1184,13 +1359,12 @@ impl<'a> SchemaWriteTransaction<'a> {
"A system created class that all objects must contain",
),
systemmay: vec![AttrString::from("description")],
may: vec![],
systemmust: vec![
AttrString::from("class"),
AttrString::from("uuid"),
AttrString::from("last_modified_cid"),
],
must: vec![],
..Default::default()
},
);
self.classes.insert(
@ -1203,9 +1377,7 @@ impl<'a> SchemaWriteTransaction<'a> {
AttrString::from("memberof"),
AttrString::from("directmemberof")
],
may: vec![],
systemmust: vec![],
must: vec![],
.. Default::default()
},
);
self.classes.insert(
@ -1216,10 +1388,7 @@ impl<'a> SchemaWriteTransaction<'a> {
description: String::from(
"A class type that has green hair and turns off all rules ...",
),
systemmay: vec![],
may: vec![],
systemmust: vec![],
must: vec![],
..Default::default()
},
);
/* These two classes are core to the entry lifecycle for recycling and tombstoning */
@ -1229,10 +1398,7 @@ impl<'a> SchemaWriteTransaction<'a> {
name: AttrString::from("recycled"),
uuid: UUID_SCHEMA_CLASS_RECYCLED,
description: String::from("An object that has been deleted, but still recoverable via the revive operation. Recycled objects are not modifiable, only revivable."),
systemmay: vec![],
may: vec![],
systemmust: vec![],
must: vec![],
.. Default::default()
},
);
self.classes.insert(
@ -1241,13 +1407,11 @@ impl<'a> SchemaWriteTransaction<'a> {
name: AttrString::from("tombstone"),
uuid: UUID_SCHEMA_CLASS_TOMBSTONE,
description: String::from("An object that is purged from the recycle bin. This is a system internal state. Tombstones have no attributes beside UUID."),
systemmay: vec![],
may: vec![],
systemmust: vec![
AttrString::from("class"),
AttrString::from("uuid"),
],
must: vec![],
.. Default::default()
},
);
// sysinfo
@ -1257,10 +1421,8 @@ impl<'a> SchemaWriteTransaction<'a> {
name: AttrString::from("system_info"),
uuid: UUID_SCHEMA_CLASS_SYSTEM_INFO,
description: String::from("System metadata object class"),
systemmay: vec![],
may: vec![],
systemmust: vec![AttrString::from("version")],
must: vec![],
..Default::default()
},
);
// ACP
@ -1274,13 +1436,18 @@ impl<'a> SchemaWriteTransaction<'a> {
AttrString::from("acp_enable"),
AttrString::from("description"),
],
may: vec![],
systemmust: vec![
AttrString::from("acp_receiver"),
AttrString::from("acp_targetscope"),
AttrString::from("name"),
],
must: vec![],
systemsupplements: vec![
AttrString::from("access_control_search"),
AttrString::from("access_control_delete"),
AttrString::from("access_control_modify"),
AttrString::from("access_control_create"),
],
..Default::default()
},
);
self.classes.insert(
@ -1289,10 +1456,8 @@ impl<'a> SchemaWriteTransaction<'a> {
name: AttrString::from("access_control_search"),
uuid: UUID_SCHEMA_CLASS_ACCESS_CONTROL_SEARCH,
description: String::from("System Access Control Search Class"),
systemmay: vec![],
may: vec![],
systemmust: vec![AttrString::from("acp_search_attr")],
must: vec![],
..Default::default()
},
);
self.classes.insert(
@ -1301,10 +1466,7 @@ impl<'a> SchemaWriteTransaction<'a> {
name: AttrString::from("access_control_delete"),
uuid: UUID_SCHEMA_CLASS_ACCESS_CONTROL_DELETE,
description: String::from("System Access Control DELETE Class"),
systemmay: vec![],
may: vec![],
systemmust: vec![],
must: vec![],
..Default::default()
},
);
self.classes.insert(
@ -1318,9 +1480,7 @@ impl<'a> SchemaWriteTransaction<'a> {
AttrString::from("acp_modify_presentattr"),
AttrString::from("acp_modify_class"),
],
may: vec![],
systemmust: vec![],
must: vec![],
..Default::default()
},
);
self.classes.insert(
@ -1333,9 +1493,7 @@ impl<'a> SchemaWriteTransaction<'a> {
AttrString::from("acp_create_class"),
AttrString::from("acp_create_attr"),
],
may: vec![],
systemmust: vec![],
must: vec![],
..Default::default()
},
);
self.classes.insert(
@ -1344,10 +1502,7 @@ impl<'a> SchemaWriteTransaction<'a> {
name: AttrString::from("system"),
uuid: UUID_SCHEMA_CLASS_SYSTEM,
description: String::from("A class denoting that a type is system generated and protected. It has special internal behaviour."),
systemmay: vec![],
may: vec![],
systemmust: vec![],
must: vec![],
.. Default::default()
},
);
@ -1435,6 +1590,7 @@ impl Schema {
}
}
#[cfg(test)]
pub(crate) fn write_blocking(&self) -> SchemaWriteTransaction<'_> {
self.write()
}
@ -2183,13 +2339,146 @@ mod tests {
uuid: Uuid::new_v4(),
description: String::from("test object"),
systemmay: vec![AttrString::from("claim")],
may: vec![],
systemmust: vec![],
must: vec![],
..Default::default()
};
assert!(schema.update_classes(vec![class]).is_ok());
assert!(schema.validate().len() == 1);
}
#[test]
fn test_schema_class_exclusion_requires() {
let _ = sketching::test_init();
let schema_outer = Schema::new().expect("failed to create schema");
let mut schema = schema_outer.write_blocking();
assert!(schema.validate().len() == 0);
// We setup some classes that have requires and excludes and check that they
// are enforced correctly.
let class_account = SchemaClass {
name: AttrString::from("account"),
uuid: Uuid::new_v4(),
description: String::from("account object"),
systemmust: vec![
AttrString::from("class"),
AttrString::from("uuid"),
AttrString::from("last_modified_cid"),
],
systemsupplements: vec![AttrString::from("service"), AttrString::from("person")],
..Default::default()
};
let class_person = SchemaClass {
name: AttrString::from("person"),
uuid: Uuid::new_v4(),
description: String::from("person object"),
systemmust: vec![
AttrString::from("class"),
AttrString::from("uuid"),
AttrString::from("last_modified_cid"),
],
..Default::default()
};
let class_service = SchemaClass {
name: AttrString::from("service"),
uuid: Uuid::new_v4(),
description: String::from("service object"),
systemmust: vec![
AttrString::from("class"),
AttrString::from("uuid"),
AttrString::from("last_modified_cid"),
],
excludes: vec![AttrString::from("person")],
..Default::default()
};
assert!(schema
.update_classes(vec![class_account, class_person, class_service])
.is_ok());
// Missing person or service account.
let e_account = unsafe {
entry_init!(
("class", Value::new_class("account")),
("uuid", Value::new_uuid(Uuid::new_v4()))
)
.into_invalid_new()
};
assert_eq!(
e_account.validate(&schema),
Err(SchemaError::SupplementsNotSatisfied(vec![
"service".to_string(),
"person".to_string(),
]))
);
// Service account missing account
/*
let e_service = unsafe { entry_init!(
("class", Value::new_class("service")),
("uuid", Value::new_uuid(Uuid::new_v4()))
).into_invalid_new() };
assert_eq!(
e_service.validate(&schema),
Err(SchemaError::RequiresNotSatisfied(vec!["account".to_string()]))
);
*/
// Service can't have person
let e_service_person = unsafe {
entry_init!(
("class", Value::new_class("service")),
("class", Value::new_class("account")),
("class", Value::new_class("person")),
("uuid", Value::new_uuid(Uuid::new_v4()))
)
.into_invalid_new()
};
assert_eq!(
e_service_person.validate(&schema),
Err(SchemaError::ExcludesNotSatisfied(
vec!["person".to_string()]
))
);
// These are valid configurations.
let e_service_valid = unsafe {
entry_init!(
("class", Value::new_class("service")),
("class", Value::new_class("account")),
("uuid", Value::new_uuid(Uuid::new_v4()))
)
.into_invalid_new()
};
assert!(e_service_valid.validate(&schema).is_ok());
let e_person_valid = unsafe {
entry_init!(
("class", Value::new_class("person")),
("class", Value::new_class("account")),
("uuid", Value::new_uuid(Uuid::new_v4()))
)
.into_invalid_new()
};
assert!(e_person_valid.validate(&schema).is_ok());
let e_person_valid = unsafe {
entry_init!(
("class", Value::new_class("person")),
("uuid", Value::new_uuid(Uuid::new_v4()))
)
.into_invalid_new()
};
assert!(e_person_valid.validate(&schema).is_ok());
}
}

View file

@ -50,6 +50,8 @@ lazy_static! {
static ref PVCLASS_ACC: PartialValue = PartialValue::new_class("access_control_create");
static ref PVCLASS_ACP: PartialValue = PartialValue::new_class("access_control_profile");
static ref PVCLASS_OAUTH2_RS: PartialValue = PartialValue::new_class("oauth2_resource_server");
static ref PVCLASS_ACCOUNT: PartialValue = PartialValue::new_class("account");
static ref PVCLASS_PERSON: PartialValue = PartialValue::new_class("person");
static ref PVUUID_DOMAIN_INFO: PartialValue = PartialValue::new_uuid(*UUID_DOMAIN_INFO);
static ref PVACP_ENABLE_FALSE: PartialValue = PartialValue::new_bool(false);
}
@ -1169,6 +1171,10 @@ impl QueryServer {
migrate_txn.migrate_5_to_6()?;
}
if system_info_version < 7 {
migrate_txn.migrate_6_to_7()?;
}
migrate_txn.commit()?;
// Migrations complete. Init idm will now set the version as needed.
@ -2275,6 +2281,23 @@ impl<'a> QueryServerWriteTransaction<'a> {
})
}
/// Migrate 6 to 7
///
/// Modify accounts that are not persons, to be service accounts so that the extension
/// rules remain valid.
pub fn migrate_6_to_7(&self) -> Result<(), OperationError> {
spanned!("server::migrate_5_to_6", {
admin_warn!("starting 6 to 7 migration.");
let filter = filter!(f_and!([
f_eq("class", (*PVCLASS_ACCOUNT).clone()),
f_andnot(f_eq("class", (*PVCLASS_PERSON).clone())),
]));
let modlist = ModifyList::new_append("class", Value::new_class("service_account"));
self.internal_modify(&filter, &modlist)
// Complete
})
}
// These are where searches and other actions are actually implemented. This
// is the "internal" version, where we define the event as being internal
// only, allowing certain plugin by passes etc.
@ -2580,6 +2603,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
JSON_SCHEMA_CLASS_ORGPERSON,
JSON_SCHEMA_CLASS_GROUP,
JSON_SCHEMA_CLASS_ACCOUNT,
JSON_SCHEMA_CLASS_SERVICE_ACCOUNT,
JSON_SCHEMA_CLASS_DOMAIN_INFO,
JSON_SCHEMA_CLASS_POSIXACCOUNT,
JSON_SCHEMA_CLASS_POSIXGROUP,
@ -2677,6 +2701,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
JSON_IDM_ACP_MANAGE_PRIV_V1,
JSON_DOMAIN_ADMINS,
JSON_IDM_HP_OAUTH2_MANAGE_PRIV_V1,
JSON_IDM_HP_SERVICE_ACCOUNT_INTO_PERSON_MIGRATE_PRIV,
// All members must exist before we write HP
JSON_IDM_HIGH_PRIVILEGE_V1,
// Built in access controls.
@ -2718,6 +2743,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
JSON_IDM_HP_ACP_OAUTH2_MANAGE_PRIV_V1,
JSON_IDM_ACP_RADIUS_SECRET_READ_PRIV_V1,
JSON_IDM_ACP_RADIUS_SECRET_WRITE_PRIV_V1,
JSON_IDM_HP_ACP_SERVICE_ACCOUNT_INTO_PERSON_MIGRATE_V1,
];
let res: Result<(), _> = idm_entries

View file

@ -233,11 +233,10 @@ impl ValueSetEmailAddress {
if let Some(primary) = primary {
Some(Box::new(ValueSetEmailAddress { primary, set }))
} else {
if let Some(primary) = set.iter().cloned().take(1).next() {
Some(Box::new(ValueSetEmailAddress { primary, set }))
} else {
None
}
set.iter()
.next()
.cloned()
.map(|primary| Box::new(ValueSetEmailAddress { primary, set }))
}
}
}
@ -269,7 +268,7 @@ impl ValueSetT for ValueSetEmailAddress {
let r = self.set.remove(a);
if &self.primary == a {
// if we can, inject another former address into primary.
if let Some(n) = self.set.iter().take(1).cloned().next() {
if let Some(n) = self.set.iter().next().cloned() {
self.primary = n
}
}

View file

@ -99,11 +99,11 @@ impl ValueSetT for ValueSetBool {
}
fn to_partialvalue_iter(&self) -> Box<dyn Iterator<Item = PartialValue> + '_> {
Box::new(self.set.iter().copied().map(|b| PartialValue::new_bool(b)))
Box::new(self.set.iter().copied().map(PartialValue::new_bool))
}
fn to_value_iter(&self) -> Box<dyn Iterator<Item = Value> + '_> {
Box::new(self.set.iter().copied().map(|b| Value::new_bool(b)))
Box::new(self.set.iter().copied().map(Value::new_bool))
}
fn equal(&self, other: &ValueSet) -> bool {

View file

@ -125,11 +125,11 @@ impl ValueSetT for ValueSetCid {
}
fn to_partialvalue_iter(&self) -> Box<dyn Iterator<Item = PartialValue> + '_> {
Box::new(self.set.iter().cloned().map(|u| PartialValue::new_cid(u)))
Box::new(self.set.iter().cloned().map(PartialValue::new_cid))
}
fn to_value_iter(&self) -> Box<dyn Iterator<Item = Value> + '_> {
Box::new(self.set.iter().cloned().map(|u| Value::new_cid(u)))
Box::new(self.set.iter().cloned().map(Value::new_cid))
}
fn equal(&self, other: &ValueSet) -> bool {

View file

@ -32,7 +32,7 @@ impl ValueSetOauthScope {
Ok(Box::new(ValueSetOauthScope { set }))
}
pub fn from_iter<'a, T>(iter: T) -> Option<Box<Self>>
pub fn from_iter<T>(iter: T) -> Option<Box<Self>>
where
T: IntoIterator<Item = String>,
{
@ -246,8 +246,7 @@ impl ValueSetT for ValueSetOauthScopeMap {
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
self.map
.values()
.map(|set| set.iter())
.flatten()
.flat_map(|set| set.iter())
.all(|s| OAUTHSCOPE_RE.is_match(s))
}

View file

@ -25,7 +25,7 @@ impl ValueSetRestricted {
Ok(Box::new(ValueSetRestricted { set }))
}
pub fn from_iter<'a, T>(iter: T) -> Option<Box<Self>>
pub fn from_iter<T>(iter: T) -> Option<Box<Self>>
where
T: IntoIterator<Item = String>,
{

View file

@ -102,16 +102,11 @@ impl ValueSetT for ValueSetUint32 {
}
fn to_partialvalue_iter(&self) -> Box<dyn Iterator<Item = PartialValue> + '_> {
Box::new(
self.set
.iter()
.copied()
.map(|b| PartialValue::new_uint32(b)),
)
Box::new(self.set.iter().copied().map(PartialValue::new_uint32))
}
fn to_value_iter(&self) -> Box<dyn Iterator<Item = Value> + '_> {
Box::new(self.set.iter().copied().map(|b| Value::new_uint32(b)))
Box::new(self.set.iter().copied().map(Value::new_uint32))
}
fn equal(&self, other: &ValueSet) -> bool {

View file

@ -105,11 +105,11 @@ impl ValueSetT for ValueSetUuid {
}
fn to_partialvalue_iter(&self) -> Box<dyn Iterator<Item = PartialValue> + '_> {
Box::new(self.set.iter().copied().map(|u| PartialValue::new_uuid(u)))
Box::new(self.set.iter().copied().map(PartialValue::new_uuid))
}
fn to_value_iter(&self) -> Box<dyn Iterator<Item = Value> + '_> {
Box::new(self.set.iter().copied().map(|u| Value::new_uuid(u)))
Box::new(self.set.iter().copied().map(Value::new_uuid))
}
fn equal(&self, other: &ValueSet) -> bool {
@ -251,11 +251,11 @@ impl ValueSetT for ValueSetRefer {
}
fn to_partialvalue_iter(&self) -> Box<dyn Iterator<Item = PartialValue> + '_> {
Box::new(self.set.iter().copied().map(|u| PartialValue::new_refer(u)))
Box::new(self.set.iter().copied().map(PartialValue::new_refer))
}
fn to_value_iter(&self) -> Box<dyn Iterator<Item = Value> + '_> {
Box::new(self.set.iter().copied().map(|u| Value::new_refer(u)))
Box::new(self.set.iter().copied().map(Value::new_refer))
}
fn equal(&self, other: &ValueSet) -> bool {

View file

@ -440,16 +440,20 @@ pub fn create_https_server(
let mut tserver_cacheable = tserver.at("");
tserver_cacheable.with(CacheableMiddleware::default());
// We allow clients to cache the unix token for accounts and groups.
let mut account_route_cacheable = tserver_cacheable.at("/v1/account");
account_route_cacheable
.at("/:id/_unix/_token")
.mapped_get(&mut routemap, account_get_id_unix_token);
// We allow caching of the radius token.
account_route_cacheable
.at("/:id/_radius/_token")
.mapped_get(&mut routemap, account_get_id_radius_token);
// We allow clients to cache the unix token.
account_route_cacheable
let mut group_route_cacheable = tserver_cacheable.at("/v1/group");
group_route_cacheable
.at("/:id/_unix/_token")
.mapped_get(&mut routemap, account_get_id_unix_token);
.mapped_get(&mut routemap, group_get_id_unix_token);
// ==== These routes can not be cached
let mut appserver = tserver.at("");
@ -573,9 +577,6 @@ pub fn create_https_server(
.at("/_credential")
.mapped_get(&mut routemap, do_nothing);
self_route
.at("/_credential/primary/set_password")
.mapped_post(&mut routemap, idm_account_set_password);
self_route
.at("/_credential/:cid/_lock")
.mapped_get(&mut routemap, do_nothing);
@ -603,87 +604,130 @@ pub fn create_https_server(
.mapped_post(&mut routemap, person_post);
person_route
.at("/:id")
.mapped_get(&mut routemap, person_id_get);
let mut account_route = appserver.at("/v1/account");
account_route
.at("/")
.mapped_get(&mut routemap, account_get)
.mapped_post(&mut routemap, account_post);
account_route
.at("/:id")
.mapped_get(&mut routemap, account_id_get)
.mapped_delete(&mut routemap, account_id_delete);
account_route
.mapped_get(&mut routemap, person_id_get)
.mapped_patch(&mut routemap, account_id_patch)
.mapped_delete(&mut routemap, person_account_id_delete);
person_route
.at("/:id/_attr/:attr")
.mapped_get(&mut routemap, account_id_get_attr)
.mapped_put(&mut routemap, account_id_put_attr)
.mapped_post(&mut routemap, account_id_post_attr)
.mapped_delete(&mut routemap, account_id_delete_attr);
account_route
.at("/:id/_person/_extend")
.mapped_post(&mut routemap, account_post_id_person_extend);
account_route
.at("/:id/_person/_set")
.mapped_post(&mut routemap, account_post_id_person_set);
account_route
person_route
.at("/:id/_lock")
.mapped_get(&mut routemap, do_nothing);
account_route
person_route
.at("/:id/_credential")
.mapped_get(&mut routemap, do_nothing);
account_route
person_route
.at("/:id/_credential/_status")
.mapped_get(&mut routemap, account_get_id_credential_status);
account_route
.at("/:id/_credential/primary")
.mapped_put(&mut routemap, account_put_id_credential_primary);
account_route
person_route
.at("/:id/_credential/:cid/_lock")
.mapped_get(&mut routemap, do_nothing);
account_route
.at("/:id/_credential/:cid/backup_code")
.mapped_get(&mut routemap, account_get_backup_code);
// .mapped_post(&mut routemap, account_post_backup_code_regenerate) // use "/:id/_credential/primary" instead
// .mapped_delete(&mut routemap, account_delete_backup_code); // same as above
account_route
person_route
.at("/:id/_credential/_update")
.mapped_get(&mut routemap, account_get_id_credential_update);
account_route
person_route
.at("/:id/_credential/_update_intent")
.mapped_get(&mut routemap, account_get_id_credential_update_intent);
account_route
person_route
.at("/:id/_credential/_update_intent/:ttl")
.mapped_get(&mut routemap, account_get_id_credential_update_intent);
account_route
person_route
.at("/:id/_ssh_pubkeys")
.mapped_get(&mut routemap, account_get_id_ssh_pubkeys)
.mapped_post(&mut routemap, account_post_id_ssh_pubkey);
account_route
person_route
.at("/:id/_ssh_pubkeys/:tag")
.mapped_get(&mut routemap, account_get_id_ssh_pubkey_tag)
.mapped_delete(&mut routemap, account_delete_id_ssh_pubkey_tag);
account_route
person_route
.at("/:id/_radius")
.mapped_get(&mut routemap, account_get_id_radius)
.mapped_post(&mut routemap, account_post_id_radius_regenerate)
.mapped_delete(&mut routemap, account_delete_id_radius);
account_route
person_route
.at("/:id/_unix")
.mapped_post(&mut routemap, account_post_id_unix);
account_route
.at("/:id/_unix/_auth")
.mapped_post(&mut routemap, account_post_id_unix_auth);
account_route
person_route
.at("/:id/_unix/_credential")
.mapped_put(&mut routemap, account_put_id_unix_credential)
.mapped_delete(&mut routemap, account_delete_id_unix_credential);
// Service accounts
let mut service_account_route = appserver.at("/v1/service_account");
service_account_route
.at("/")
.mapped_get(&mut routemap, service_account_get)
.mapped_post(&mut routemap, service_account_post);
service_account_route
.at("/:id")
.mapped_get(&mut routemap, service_account_id_get)
.mapped_patch(&mut routemap, account_id_patch)
.mapped_delete(&mut routemap, service_account_id_delete);
service_account_route
.at("/:id/_attr/:attr")
.mapped_get(&mut routemap, account_id_get_attr)
.mapped_put(&mut routemap, account_id_put_attr)
.mapped_post(&mut routemap, account_id_post_attr)
.mapped_delete(&mut routemap, account_id_delete_attr);
service_account_route
.at("/:id/_lock")
.mapped_get(&mut routemap, do_nothing);
service_account_route
.at("/:id/_into_person")
.mapped_post(&mut routemap, service_account_into_person);
service_account_route
.at("/:id/_credential")
.mapped_get(&mut routemap, do_nothing);
service_account_route
.at("/:id/_credential/_generate")
.mapped_get(&mut routemap, service_account_credential_generate);
service_account_route
.at("/:id/_credential/_status")
.mapped_get(&mut routemap, account_get_id_credential_status);
service_account_route
.at("/:id/_credential/:cid/_lock")
.mapped_get(&mut routemap, do_nothing);
service_account_route
.at("/:id/_ssh_pubkeys")
.mapped_get(&mut routemap, account_get_id_ssh_pubkeys)
.mapped_post(&mut routemap, account_post_id_ssh_pubkey);
service_account_route
.at("/:id/_ssh_pubkeys/:tag")
.mapped_get(&mut routemap, account_get_id_ssh_pubkey_tag)
.mapped_delete(&mut routemap, account_delete_id_ssh_pubkey_tag);
service_account_route
.at("/:id/_unix")
.mapped_post(&mut routemap, account_post_id_unix);
// TODO: Apis for token management
// Shared account features only - mainly this is for unix-like
// features.
let mut account_route = appserver.at("/v1/account");
account_route
.at("/:id/_unix/_auth")
.mapped_post(&mut routemap, account_post_id_unix_auth);
account_route
.at("/:id/_ssh_pubkeys")
.mapped_get(&mut routemap, account_get_id_ssh_pubkeys);
account_route
.at("/:id/_ssh_pubkeys/:tag")
.mapped_get(&mut routemap, account_get_id_ssh_pubkey_tag);
// Credential updates, don't require the account id.
let mut cred_route = appserver.at("/v1/credential");
cred_route
.at("/_exchange_intent")
@ -723,9 +767,6 @@ pub fn create_https_server(
group_route
.at("/:id/_unix")
.mapped_post(&mut routemap, group_post_id_unix);
group_route
.at("/:id/_unix/_token")
.mapped_get(&mut routemap, group_get_id_unix_token);
let mut domain_route = appserver.at("/v1/domain");
domain_route.at("/").mapped_get(&mut routemap, domain_get);

View file

@ -6,9 +6,9 @@ use kanidm::status::StatusRequestEvent;
use kanidm_proto::v1::Entry as ProtoEntry;
use kanidm_proto::v1::{
AccountPersonSet, AccountUnixExtend, AuthRequest, AuthResponse, AuthState as ProtoAuthState,
CUIntentToken, CURequest, CUSessionToken, CreateRequest, DeleteRequest, GroupUnixExtend,
ModifyRequest, OperationError, SearchRequest, SetCredentialRequest, SingleStringRequest,
AccountUnixExtend, AuthRequest, AuthResponse, AuthState as ProtoAuthState, CUIntentToken,
CURequest, CUSessionToken, CreateRequest, DeleteRequest, GroupUnixExtend, ModifyRequest,
OperationError, SearchRequest, SingleStringRequest,
};
use super::{to_tide_response, AppState, RequestExtensions, RouteMap};
@ -260,20 +260,6 @@ pub async fn json_rest_event_delete_attr(
}
}
pub async fn json_rest_event_credential_put(mut req: tide::Request<AppState>) -> tide::Result {
let uat = req.get_current_uat();
let uuid_or_name = req.get_url_param("id")?;
let sac: SetCredentialRequest = req.body_json().await?;
let (eventid, hvalue) = req.new_eventid();
let res = req
.state()
.qe_w_ref
.handle_credentialset(uat, uuid_or_name, sac, eventid)
.await;
to_tide_response(res, hvalue)
}
// Okay, so a put normally needs
// * filter of what we are working on (id + class)
// * a Map<String, Vec<String>> that we turn into a modlist.
@ -357,7 +343,11 @@ pub async fn person_get(req: tide::Request<AppState>) -> tide::Result {
}
pub async fn person_post(req: tide::Request<AppState>) -> tide::Result {
let classes = vec!["person".to_string(), "object".to_string()];
let classes = vec![
"person".to_string(),
"account".to_string(),
"object".to_string(),
];
json_rest_event_post(req, classes).await
}
@ -366,23 +356,70 @@ pub async fn person_id_get(req: tide::Request<AppState>) -> tide::Result {
json_rest_event_get_id(req, filter, None).await
}
pub async fn person_account_id_delete(req: tide::Request<AppState>) -> tide::Result {
let filter = filter_all!(f_eq("class", PartialValue::new_class("person")));
json_rest_event_delete_id(req, filter).await
}
// == account ==
pub async fn account_get(req: tide::Request<AppState>) -> tide::Result {
let filter = filter_all!(f_eq("class", PartialValue::new_class("account")));
pub async fn service_account_get(req: tide::Request<AppState>) -> tide::Result {
let filter = filter_all!(f_eq("class", PartialValue::new_class("service_account")));
json_rest_event_get(req, filter, None).await
}
pub async fn account_post(req: tide::Request<AppState>) -> tide::Result {
let classes = vec!["account".to_string(), "object".to_string()];
pub async fn service_account_post(req: tide::Request<AppState>) -> tide::Result {
let classes = vec![
"service_account".to_string(),
"account".to_string(),
"object".to_string(),
];
json_rest_event_post(req, classes).await
}
pub async fn account_id_get(req: tide::Request<AppState>) -> tide::Result {
let filter = filter_all!(f_eq("class", PartialValue::new_class("account")));
pub async fn service_account_id_get(req: tide::Request<AppState>) -> tide::Result {
let filter = filter_all!(f_eq("class", PartialValue::new_class("service_account")));
json_rest_event_get_id(req, filter, None).await
}
pub async fn service_account_id_delete(req: tide::Request<AppState>) -> tide::Result {
let filter = filter_all!(f_eq("class", PartialValue::new_class("service_account")));
json_rest_event_delete_id(req, filter).await
}
pub async fn service_account_credential_generate(req: tide::Request<AppState>) -> tide::Result {
let uat = req.get_current_uat();
let uuid_or_name = req.get_url_param("id")?;
let (eventid, hvalue) = req.new_eventid();
let res = req
.state()
.qe_w_ref
.handle_service_account_credential_generate(uat, uuid_or_name, eventid)
.await;
to_tide_response(res, hvalue)
}
// Due to how the migrations work in 6 -> 7, we can accidentally
// mark "accounts" as service accounts when they are persons. This
// allows migrating them to the person type due to it's similarities.
//
// In the future this will be REMOVED!
pub async fn service_account_into_person(req: tide::Request<AppState>) -> tide::Result {
let uat = req.get_current_uat();
let uuid_or_name = req.get_url_param("id")?;
let (eventid, hvalue) = req.new_eventid();
let res = req
.state()
.qe_w_ref
.handle_service_account_into_person(uat, uuid_or_name, eventid)
.await;
to_tide_response(res, hvalue)
}
pub async fn account_id_get_attr(req: tide::Request<AppState>) -> tide::Result {
let filter = filter_all!(f_eq("class", PartialValue::new_class("account")));
json_rest_event_get_id_attr(req, filter).await
@ -404,13 +441,24 @@ pub async fn account_id_put_attr(req: tide::Request<AppState>) -> tide::Result {
json_rest_event_put_id_attr(req, filter).await
}
pub async fn account_id_delete(req: tide::Request<AppState>) -> tide::Result {
let filter = filter_all!(f_eq("class", PartialValue::new_class("account")));
json_rest_event_delete_id(req, filter).await
}
pub async fn account_id_patch(mut req: tide::Request<AppState>) -> tide::Result {
// Update a value / attrs
let uat = req.get_current_uat();
let id = req.get_url_param("id")?;
pub async fn account_put_id_credential_primary(req: tide::Request<AppState>) -> tide::Result {
json_rest_event_credential_put(req).await
let obj: ProtoEntry = req.body_json().await?;
let filter = filter_all!(f_eq("class", PartialValue::new_class("account")));
let filter = Filter::join_parts_and(filter, filter_all!(f_id(id.as_str())));
let (eventid, hvalue) = req.new_eventid();
let res = req
.state()
.qe_w_ref
.handle_internalpatch(uat, filter, obj, eventid)
.await;
to_tide_response(res, hvalue)
}
pub async fn account_get_id_credential_update(req: tide::Request<AppState>) -> tide::Result {
@ -526,20 +574,6 @@ pub async fn account_get_id_credential_status(req: tide::Request<AppState>) -> t
to_tide_response(res, hvalue)
}
pub async fn account_get_backup_code(req: tide::Request<AppState>) -> tide::Result {
let uat = req.get_current_uat();
let uuid_or_name = req.get_url_param("id")?;
let (eventid, hvalue) = req.new_eventid();
let res = req
.state()
.qe_r_ref
.handle_idmbackupcodeview(uat, uuid_or_name, eventid)
.await;
to_tide_response(res, hvalue)
}
// Return a vec of str
pub async fn account_get_id_ssh_pubkeys(req: tide::Request<AppState>) -> tide::Result {
let uat = req.get_current_uat();
@ -654,32 +688,6 @@ pub async fn account_get_id_radius_token(req: tide::Request<AppState>) -> tide::
to_tide_response(res, hvalue)
}
pub async fn account_post_id_person_extend(mut req: tide::Request<AppState>) -> tide::Result {
let uat = req.get_current_uat();
let uuid_or_name = req.get_url_param("id")?;
let obj: AccountPersonSet = req.body_json().await?;
let (eventid, hvalue) = req.new_eventid();
let res = req
.state()
.qe_w_ref
.handle_idmaccountpersonextend(uat, uuid_or_name, obj, eventid)
.await;
to_tide_response(res, hvalue)
}
pub async fn account_post_id_person_set(mut req: tide::Request<AppState>) -> tide::Result {
let uat = req.get_current_uat();
let uuid_or_name = req.get_url_param("id")?;
let obj: AccountPersonSet = req.body_json().await?;
let (eventid, hvalue) = req.new_eventid();
let res = req
.state()
.qe_w_ref
.handle_idmaccountpersonset(uat, uuid_or_name, obj, eventid)
.await;
to_tide_response(res, hvalue)
}
pub async fn account_post_id_unix(mut req: tide::Request<AppState>) -> tide::Result {
let uat = req.get_current_uat();
let uuid_or_name = req.get_url_param("id")?;
@ -1037,19 +1045,6 @@ pub async fn auth_valid(req: tide::Request<AppState>) -> tide::Result {
to_tide_response(res, hvalue)
}
pub async fn idm_account_set_password(mut req: tide::Request<AppState>) -> tide::Result {
let uat = req.get_current_uat();
let obj: SingleStringRequest = req.body_json().await?;
let cleartext = obj.value;
let (eventid, hvalue) = req.new_eventid();
let res = req
.state()
.qe_w_ref
.handle_idmaccountsetpassword(uat, cleartext, eventid)
.await;
to_tide_response(res, hvalue)
}
// == Status
pub async fn status(req: tide::Request<AppState>) -> tide::Result {

View file

@ -57,7 +57,7 @@ static DEFAULT_NOT_HP_GROUP_NAMES: [&str; 2] =
["idm_account_unix_extend_priv", "idm_group_unix_extend_priv"];
async fn create_user(rsclient: &KanidmClient, id: &str, group_name: &str) -> () {
rsclient.idm_account_create(id, id).await.unwrap();
rsclient.idm_person_account_create(id, id).await.unwrap();
// Create group and add to user to test read attr: member_of
if rsclient.idm_group_get(&group_name).await.unwrap().is_none() {
@ -81,13 +81,13 @@ async fn is_attr_writable(rsclient: &KanidmClient, id: &str, attr: &str) -> Opti
),
"primary_credential" => Some(
rsclient
.idm_account_primary_credential_set_password(id, "dsadjasiodqwjk12asdl")
.idm_person_account_primary_credential_set_password(id, "dsadjasiodqwjk12asdl")
.await
.is_ok(),
),
"ssh_publickey" => Some(
rsclient
.idm_account_post_ssh_pubkey(
.idm_person_account_post_ssh_pubkey(
id,
"k1",
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAeGW1P6Pc2rPq0XqbRaDKBcXZUPRklo0\
@ -98,19 +98,19 @@ async fn is_attr_writable(rsclient: &KanidmClient, id: &str, attr: &str) -> Opti
),
"unix_password" => Some(
rsclient
.idm_account_unix_cred_put(id, "dsadjasiodqwjk12asdl")
.idm_person_account_unix_cred_put(id, "dsadjasiodqwjk12asdl")
.await
.is_ok(),
),
"legalname" => Some(
rsclient
.idm_account_person_set(id, None, Some("test legal name".into()))
.idm_person_account_set_attr(id, "legalname", &["test legal name"])
.await
.is_ok(),
),
"mail" => Some(
rsclient
.idm_account_person_set(id, Some(&[format!("{}@example.com", id)]), None)
.idm_person_account_set_attr(id, "mail", &[&format!("{}@example.com", id)])
.await
.is_ok(),
),
@ -130,14 +130,15 @@ async fn is_attr_writable(rsclient: &KanidmClient, id: &str, attr: &str) -> Opti
}
}
async fn add_all_attrs(rsclient: &KanidmClient, id: &str, group_name: &str) {
async fn add_all_attrs(
rsclient: &KanidmClient,
id: &str,
group_name: &str,
legalname: Option<&str>,
) {
// Extend with posix attrs to test read attr: gidnumber and loginshell
rsclient
.idm_group_add_members("idm_admins", &[ADMIN_TEST_USER])
.await
.unwrap();
rsclient
.idm_account_unix_extend(id, None, Some(&"/bin/sh"))
.idm_person_account_unix_extend(id, None, Some(&"/bin/sh"))
.await
.unwrap();
rsclient
@ -145,16 +146,16 @@ async fn add_all_attrs(rsclient: &KanidmClient, id: &str, group_name: &str) {
.await
.unwrap();
// Extend with person to allow legalname
rsclient
.idm_account_person_extend(id, None, None)
.await
.unwrap();
for attr in ["ssh_publickey", "legalname", "mail"].iter() {
for attr in ["ssh_publickey", "mail"].iter() {
assert!(is_attr_writable(&rsclient, id, attr).await.unwrap());
}
if let Some(legalname) = legalname {
assert!(is_attr_writable(&rsclient, legalname, "legalname")
.await
.unwrap());
}
// Write radius credentials
if id != "anonymous" {
login_account(&rsclient, id).await;
@ -178,7 +179,7 @@ async fn create_user_with_all_attrs(
let group_name = optional_group.unwrap_or(&group_format);
create_user(&rsclient, id, group_name).await;
add_all_attrs(&rsclient, id, group_name).await;
add_all_attrs(&rsclient, id, group_name, Some(id)).await;
}
async fn login_account(rsclient: &KanidmClient, id: &str) -> () {
@ -195,7 +196,7 @@ async fn login_account(rsclient: &KanidmClient, id: &str) -> () {
.unwrap();
rsclient
.idm_account_primary_credential_set_password(id, "eicieY7ahchaoCh0eeTa")
.idm_person_account_primary_credential_set_password(id, "eicieY7ahchaoCh0eeTa")
.await
.unwrap();
@ -292,6 +293,10 @@ async fn test_default_entries_rbac_users() {
.auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
.await
.unwrap();
rsclient
.idm_group_add_members("idm_admins", &[ADMIN_TEST_USER])
.await
.unwrap();
create_user_with_all_attrs(&rsclient, "self_account", Some("self_group")).await;
create_user_with_all_attrs(&rsclient, "other_account", Some("other_group")).await;
@ -329,6 +334,10 @@ async fn test_default_entries_rbac_account_managers() {
.auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
.await
.unwrap();
rsclient
.idm_group_add_members("idm_admins", &[ADMIN_TEST_USER])
.await
.unwrap();
create_user(&rsclient, "account_manager", "idm_account_manage_priv").await;
create_user_with_all_attrs(&rsclient, "test", Some("test_group")).await;
@ -361,6 +370,10 @@ async fn test_default_entries_rbac_group_managers() {
.auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
.await
.unwrap();
rsclient
.idm_group_add_members("idm_admins", &[ADMIN_TEST_USER])
.await
.unwrap();
create_user(&rsclient, "group_manager", "idm_group_manage_priv").await;
// create test user without creating new groups
@ -405,6 +418,11 @@ async fn test_default_entries_rbac_admins_access_control_entries() {
.auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
.await
.unwrap();
rsclient
.idm_group_add_members("idm_admins", &[ADMIN_TEST_USER])
.await
.unwrap();
static ACP_COMMON_ATTRS: [&str; 4] = ["name", "description", "acp_receiver", "acp_targetscope"];
static ACP_ENTRIES: [&str; 28] = [
"idm_admins_acp_recycle_search",
@ -452,6 +470,11 @@ async fn test_default_entries_rbac_admins_schema_entries() {
.auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
.await
.unwrap();
rsclient
.idm_group_add_members("idm_admins", &[ADMIN_TEST_USER])
.await
.unwrap();
let default_classnames: HashSet<String> = [
"access_control_create",
"access_control_delete",
@ -563,6 +586,11 @@ async fn test_default_entries_rbac_admins_group_entries() {
.auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
.await
.unwrap();
rsclient
.idm_group_add_members("idm_admins", &[ADMIN_TEST_USER])
.await
.unwrap();
create_user(&rsclient, "test", "test_group").await;
let default_group_names =
@ -579,6 +607,10 @@ async fn test_default_entries_rbac_admins_ha_accounts() {
.auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
.await
.unwrap();
rsclient
.idm_group_add_members("idm_admins", &[ADMIN_TEST_USER])
.await
.unwrap();
static MAIN_ATTRS: [&str; 3] = ["name", "displayname", "primary_credential"];
test_write_attrs(&rsclient, "idm_admin", &MAIN_ATTRS, true).await;
@ -592,12 +624,17 @@ async fn test_default_entries_rbac_admins_recycle_accounts() {
.auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
.await
.unwrap();
rsclient
.idm_group_add_members("idm_admins", &[ADMIN_TEST_USER])
.await
.unwrap();
create_user(&rsclient, "test", "test_group").await;
rsclient.idm_account_delete("test").await.unwrap();
rsclient.idm_person_account_delete("test").await.unwrap();
rsclient.recycle_bin_revive("test").await.unwrap();
let acc = rsclient.idm_account_get("test").await.unwrap();
let acc = rsclient.idm_person_account_get("test").await.unwrap();
assert!(acc.is_some());
}
@ -611,6 +648,10 @@ async fn test_default_entries_rbac_people_managers() {
.auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
.await
.unwrap();
rsclient
.idm_group_add_members("idm_admins", &[ADMIN_TEST_USER])
.await
.unwrap();
create_user(&rsclient, "read_people_manager", "idm_people_read_priv").await;
create_user_with_all_attrs(&rsclient, "test", Some("test_group")).await;
@ -650,12 +691,17 @@ async fn test_default_entries_rbac_anonymous_entry() {
.auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
.await
.unwrap();
rsclient
.idm_group_add_members("idm_admins", &[ADMIN_TEST_USER])
.await
.unwrap();
create_user_with_all_attrs(&rsclient, "test", Some("test_group")).await;
rsclient
.idm_group_add_members("test_group", &["anonymous"])
.await
.unwrap();
add_all_attrs(&rsclient, "anonymous", "test_group").await;
add_all_attrs(&rsclient, "anonymous", "test_group", None).await;
let _ = rsclient.logout();
rsclient.auth_anonymous().await.unwrap();
@ -676,6 +722,11 @@ async fn test_default_entries_rbac_radius_servers() {
.auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
.await
.unwrap();
rsclient
.idm_group_add_members("idm_admins", &[ADMIN_TEST_USER])
.await
.unwrap();
create_user(&rsclient, "radius_server", "idm_radius_servers").await;
create_user_with_all_attrs(&rsclient, "test", Some("test_group")).await;
@ -694,6 +745,10 @@ async fn test_self_write_mail_priv_people() {
.auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
.await
.unwrap();
rsclient
.idm_group_add_members("idm_admins", &[ADMIN_TEST_USER])
.await
.unwrap();
// test and other, each can write to themselves, but not each other
create_user_with_all_attrs(&rsclient, "test", None).await;
@ -703,7 +758,7 @@ async fn test_self_write_mail_priv_people() {
.await
.unwrap();
// a non-person, they can't write to themselves even with the priv
create_user(&rsclient, "nonperson", "idm_people_self_write_mail_priv").await;
create_user(&rsclient, "nonperson", "nonperson_group").await;
login_account(&rsclient, "test").await;
// can write to own mail

View file

@ -57,18 +57,24 @@ async fn test_oauth2_openid_basic_flow() {
// Extend the admin account with extended details for openid claims.
rsclient
.idm_group_add_members("idm_hp_people_extend_priv", &["admin"])
.idm_group_add_members("idm_admins", &["admin"])
.await
.unwrap();
rsclient
.idm_account_person_extend(
"admin",
Some(&["admin@localhost".to_string()]),
Some("Admin Istrator"),
)
.idm_person_account_create("oauth_test", "oauth_test")
.await
.expect("Failed to extend account details");
.expect("Failed to create account details");
rsclient
.idm_person_account_set_attr("oauth_test", "mail", &["oauth_test@localhost"])
.await
.expect("Failed to create account mail");
rsclient
.idm_person_account_primary_credential_set_password("oauth_test", ADMIN_TEST_PASSWORD)
.await
.expect("Failed to configure account password");
rsclient
.idm_oauth2_rs_update(
@ -100,10 +106,10 @@ async fn test_oauth2_openid_basic_flow() {
// Get our admin's auth token for our new client.
// We have to re-auth to update the mail field.
let res = rsclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
.auth_simple_password("oauth_test", ADMIN_TEST_PASSWORD)
.await;
assert!(res.is_ok());
let admin_uat = rsclient
let oauth_test_uat = rsclient
.get_token()
.await
.expect("No user auth token found");
@ -198,7 +204,7 @@ async fn test_oauth2_openid_basic_flow() {
let response = client
.get(format!("{}/oauth2/authorise", url))
.bearer_auth(admin_uat.clone())
.bearer_auth(oauth_test_uat.clone())
.query(&[
("response_type", "code"),
("client_id", "test_integration"),
@ -232,7 +238,7 @@ async fn test_oauth2_openid_basic_flow() {
let response = client
.get(format!("{}/oauth2/authorise/permit", url))
.bearer_auth(admin_uat)
.bearer_auth(oauth_test_uat)
.query(&[("token", consent_token.as_str())])
.send()
.await
@ -317,7 +323,7 @@ async fn test_oauth2_openid_basic_flow() {
assert!(tir.active);
assert!(tir.scope.is_some());
assert!(tir.client_id.as_deref() == Some("test_integration"));
assert!(tir.username.as_deref() == Some("admin@localhost"));
assert!(tir.username.as_deref() == Some("oauth_test@localhost"));
assert!(tir.token_type.as_deref() == Some("access_token"));
assert!(tir.exp.is_some());
assert!(tir.iat.is_some());
@ -338,7 +344,8 @@ async fn test_oauth2_openid_basic_flow() {
// This is mostly checked inside of idm/oauth2.rs. This is more to check the oidc
// token and the userinfo endpoints.
assert!(oidc.iss == Url::parse(&format!("{}/oauth2/openid/test_integration", url)).unwrap());
assert!(oidc.s_claims.email.as_deref() == Some("admin@localhost"));
eprintln!("{:?}", oidc.s_claims.email);
assert!(oidc.s_claims.email.as_deref() == Some("oauth_test@localhost"));
assert!(oidc.s_claims.email_verified == Some(true));
let response = client

View file

@ -11,7 +11,6 @@ use crate::common::{setup_async_test, ADMIN_TEST_PASSWORD};
use webauthn_authenticator_rs::{softpasskey::SoftPasskey, WebauthnAuthenticator};
const ADMIN_TEST_PASSWORD_CHANGE: &str = "integration test admin new🎉";
const UNIX_TEST_PASSWORD: &str = "unix test user password";
#[tokio::test]
@ -20,9 +19,9 @@ async fn test_server_create() {
let e: Entry = serde_json::from_str(
r#"{
"attrs": {
"class": ["account"],
"name": ["testperson"],
"displayname": ["testperson"]
"class": ["account", "service_account"],
"name": ["testaccount"],
"displayname": ["testaccount"]
}
}"#,
)
@ -139,123 +138,6 @@ async fn test_server_search() {
assert!(name == &vec!["admin".to_string()]);
}
#[tokio::test]
async fn test_server_admin_change_simple_password() {
let rsclient = setup_async_test().await;
// First show we are un-authenticated.
let pre_res = rsclient.whoami().await;
// This means it was okay whoami, but no uat attached.
assert!(pre_res.unwrap().is_none());
let res = rsclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
.await;
assert!(res.is_ok());
// Now change the password.
rsclient
.idm_account_set_password(ADMIN_TEST_PASSWORD_CHANGE.to_string())
.await
.unwrap();
// Now "reset" the client.
let _ = rsclient.logout().await;
// New password works!
let res = rsclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD_CHANGE)
.await;
assert!(res.is_ok());
// On the admin, show our credential state.
let cred_state = rsclient
.idm_account_get_credential_status("admin")
.await
.unwrap();
// Check the creds are what we expect.
if cred_state.creds.len() != 1 {
assert!(false);
}
if let Some(cred) = cred_state.creds.get(0) {
assert!(cred.type_ == CredentialDetailType::Password)
} else {
assert!(false);
}
// Old password fails, check after to prevent soft-locking.
let res = rsclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
.await;
assert!(res.is_err());
}
// Add a test for resetting another accounts pws via the rest api
#[tokio::test]
async fn test_server_admin_reset_simple_password() {
let rsclient = setup_async_test().await;
let res = rsclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
.await;
assert!(res.is_ok());
// Create a diff account
let e: Entry = serde_json::from_str(
r#"{
"attrs": {
"class": ["account"],
"name": ["testperson"],
"displayname": ["testperson"]
}
}"#,
)
.unwrap();
// Not logged in - should fail!
let res = rsclient.create(vec![e]).await;
assert!(res.is_ok());
// By default, admin's can't actually administer accounts, so mod them into
// the account admin group.
let f = Filter::Eq("name".to_string(), "idm_admins".to_string());
let m = ModifyList::new_list(vec![Modify::Present(
"member".to_string(),
"system_admins".to_string(),
)]);
let res = rsclient.modify(f, m).await;
assert!(res.is_ok());
// Now set it's password - should be rejected based on low quality
let res = rsclient
.idm_account_primary_credential_set_password("testperson", "password")
.await;
assert!(res.is_err());
// Set the password to ensure it's good
let res = rsclient
.idm_account_primary_credential_set_password(
"testperson",
"tai4eCohtae9aegheo3Uw0oobahVighaig6heeli",
)
.await;
assert!(res.is_ok());
// Check it stuck.
let tclient = rsclient.new_session().expect("failed to build new session");
assert!(tclient
.auth_simple_password("testperson", "tai4eCohtae9aegheo3Uw0oobahVighaig6heeli")
.await
.is_ok());
// Generate a pw instead
let res = rsclient
.idm_account_primary_credential_set_generated("testperson")
.await;
assert!(res.is_ok());
let gpw = res.unwrap();
let tclient = rsclient.new_session().expect("failed to build new session");
assert!(tclient
.auth_simple_password("testperson", gpw.as_str())
.await
.is_ok());
}
// test the rest group endpoint.
#[tokio::test]
async fn test_server_rest_group_read() {
@ -358,10 +240,10 @@ async fn test_server_rest_account_read() {
assert!(res.is_ok());
// List the accounts
let a_list = rsclient.idm_account_list().await.unwrap();
let a_list = rsclient.idm_service_account_list().await.unwrap();
assert!(!a_list.is_empty());
let a = rsclient.idm_account_get("admin").await.unwrap();
let a = rsclient.idm_service_account_get("admin").await.unwrap();
assert!(a.is_some());
println!("{:?}", a);
}
@ -403,22 +285,15 @@ async fn test_server_radius_credential_lifecycle() {
.await;
assert!(res.is_ok());
// self management of credentials is only for persons.
// All admin to create persons.
rsclient
.idm_account_person_extend("admin", None, None)
.idm_group_add_members("idm_admins", &["admin"])
.await
.unwrap();
let f = Filter::Eq("name".to_string(), "idm_admins".to_string());
let m = ModifyList::new_list(vec![Modify::Present(
"member".to_string(),
"system_admins".to_string(),
)]);
let res = rsclient.modify(f, m).await;
assert!(res.is_ok());
// self management of credentials is only for persons.
rsclient
.idm_account_create("demo_account", "Deeeeemo")
.idm_person_account_create("demo_account", "Deeeeemo")
.await
.unwrap();
@ -475,7 +350,7 @@ async fn test_server_radius_credential_lifecycle() {
}
#[tokio::test]
async fn test_server_rest_account_lifecycle() {
async fn test_server_rest_person_account_lifecycle() {
let rsclient = setup_async_test().await;
let res = rsclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
@ -484,40 +359,46 @@ async fn test_server_rest_account_lifecycle() {
// 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("idm_account_write_priv", &["admin"])
.idm_group_add_members("idm_admins", &["admin"])
.await
.unwrap();
// Create a new account
rsclient
.idm_account_create("demo_account", "Deeeeemo")
.idm_person_account_create("demo_account", "Deeeeemo")
.await
.unwrap();
// View the account
rsclient.idm_account_get("demo_account").await.unwrap();
rsclient
.idm_person_account_get("demo_account")
.await
.unwrap();
// change the name?
rsclient
.idm_account_set_displayname("demo_account", "Demo Account")
.idm_person_account_set_attr("demo_account", "displayname", &["Demo Account"])
.await
.unwrap();
// Test adding some mail addrs
rsclient
.idm_account_add_attr("demo_account", "mail", &["demo@idm.example.com"])
.idm_person_account_add_attr("demo_account", "mail", &["demo@idm.example.com"])
.await
.unwrap();
let r = rsclient
.idm_account_get_attr("demo_account", "mail")
.idm_person_account_get_attr("demo_account", "mail")
.await
.unwrap();
assert!(r == Some(vec!["demo@idm.example.com".to_string()]));
// Delete the account
rsclient.idm_account_delete("demo_account").await.unwrap();
rsclient
.idm_person_account_delete("demo_account")
.await
.unwrap();
}
#[tokio::test]
@ -539,13 +420,13 @@ async fn test_server_rest_sshkey_lifecycle() {
// Post an invalid key (should error)
let r1 = rsclient
.idm_account_post_ssh_pubkey("admin", "inv", "invalid key")
.idm_service_account_post_ssh_pubkey("admin", "inv", "invalid key")
.await;
assert!(r1.is_err());
// Post a valid key
let r2 = rsclient
.idm_account_post_ssh_pubkey("admin", "k1", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAeGW1P6Pc2rPq0XqbRaDKBcXZUPRklo0L1EyR30CwoP william@amethyst").await;
.idm_service_account_post_ssh_pubkey("admin", "k1", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAeGW1P6Pc2rPq0XqbRaDKBcXZUPRklo0L1EyR30CwoP william@amethyst").await;
println!("{:?}", r2);
assert!(r2.is_ok());
@ -555,7 +436,7 @@ async fn test_server_rest_sshkey_lifecycle() {
// Post a valid key
let r3 = rsclient
.idm_account_post_ssh_pubkey("admin", "k2", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBx4TpJYQjd0YI5lQIHqblIsCIK5NKVFURYS/eM3o6/Z william@amethyst").await;
.idm_service_account_post_ssh_pubkey("admin", "k2", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBx4TpJYQjd0YI5lQIHqblIsCIK5NKVFURYS/eM3o6/Z william@amethyst").await;
assert!(r3.is_ok());
// Get, should have both keys.
@ -563,7 +444,9 @@ async fn test_server_rest_sshkey_lifecycle() {
assert!(sk3.len() == 2);
// Delete a key (by tag)
let r4 = rsclient.idm_account_delete_ssh_pubkey("admin", "k1").await;
let r4 = rsclient
.idm_service_account_delete_ssh_pubkey("admin", "k1")
.await;
assert!(r4.is_ok());
// Get, should have remaining key.
@ -622,13 +505,13 @@ async fn test_server_rest_posix_lifecycle() {
// Create a new account
rsclient
.idm_account_create("posix_account", "Posix Demo Account")
.idm_person_account_create("posix_account", "Posix Demo Account")
.await
.unwrap();
// Extend the account with posix attrs.
rsclient
.idm_account_unix_extend("posix_account", None, None)
.idm_person_account_unix_extend("posix_account", None, None)
.await
.unwrap();
@ -723,19 +606,19 @@ async fn test_server_rest_posix_auth_lifecycle() {
// Setup a unix user
rsclient
.idm_account_create("posix_account", "Posix Demo Account")
.idm_person_account_create("posix_account", "Posix Demo Account")
.await
.unwrap();
// Extend the account with posix attrs.
rsclient
.idm_account_unix_extend("posix_account", None, None)
.idm_person_account_unix_extend("posix_account", None, None)
.await
.unwrap();
// add their password (unix self)
rsclient
.idm_account_unix_cred_put("posix_account", UNIX_TEST_PASSWORD)
.idm_person_account_unix_cred_put("posix_account", UNIX_TEST_PASSWORD)
.await
.unwrap();
@ -763,7 +646,7 @@ async fn test_server_rest_posix_auth_lifecycle() {
// clear password? (unix self)
rsclient
.idm_account_unix_cred_delete("posix_account")
.idm_person_account_unix_cred_delete("posix_account")
.await
.unwrap();
@ -793,18 +676,21 @@ async fn test_server_rest_recycle_lifecycle() {
// Setup a unix user
rsclient
.idm_account_create("recycle_account", "Recycle Demo Account")
.idm_person_account_create("recycle_account", "Recycle Demo Account")
.await
.unwrap();
// delete them
rsclient
.idm_account_delete("recycle_account")
.idm_person_account_delete("recycle_account")
.await
.unwrap();
// not there
let acc = rsclient.idm_account_get("recycle_account").await.unwrap();
let acc = rsclient
.idm_person_account_get("recycle_account")
.await
.unwrap();
assert!(acc.is_none());
// list the recycle bin
@ -822,7 +708,10 @@ async fn test_server_rest_recycle_lifecycle() {
.unwrap();
// they are there!
let acc = rsclient.idm_account_get("recycle_account").await.unwrap();
let acc = rsclient
.idm_person_account_get("recycle_account")
.await
.unwrap();
assert!(acc.is_some());
}
@ -840,25 +729,19 @@ async fn test_server_rest_account_import_password() {
.await
.unwrap();
rsclient
.idm_group_add_members("idm_people_extend_priv", &["admin"])
.idm_group_add_members("idm_admins", &["admin"])
.await
.unwrap();
// Create a new account
// Create a new person
rsclient
.idm_account_create("demo_account", "Deeeeemo")
.await
.unwrap();
// Make them a person, so we can import the password
rsclient
.idm_account_person_extend("demo_account", None, None)
.idm_person_account_create("demo_account", "Deeeeemo")
.await
.unwrap();
// Attempt to import a bad password
let r = rsclient
.idm_account_primary_credential_import_password("demo_account", "password")
.idm_person_account_primary_credential_import_password("demo_account", "password")
.await;
assert!(r.is_err());
@ -866,7 +749,7 @@ async fn test_server_rest_account_import_password() {
// eicieY7ahchaoCh0eeTa
// pbkdf2_sha256$36000$xIEozuZVAoYm$uW1b35DUKyhvQAf1mBqMvoBDcqSD06juzyO/nmyV0+w=
rsclient
.idm_account_primary_credential_import_password(
.idm_person_account_primary_credential_import_password(
"demo_account",
"pbkdf2_sha256$36000$xIEozuZVAoYm$uW1b35DUKyhvQAf1mBqMvoBDcqSD06juzyO/nmyV0+w=",
)
@ -883,7 +766,7 @@ async fn test_server_rest_account_import_password() {
// And that the account can self read the cred status.
let cred_state = rsclient
.idm_account_get_credential_status("demo_account")
.idm_person_account_get_credential_status("demo_account")
.await
.unwrap();
@ -894,174 +777,6 @@ async fn test_server_rest_account_import_password() {
}
}
#[tokio::test]
async fn test_server_rest_totp_auth_lifecycle() {
let rsclient = setup_async_test().await;
let res = rsclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
.await;
assert!(res.is_ok());
// Not recommended in production!
rsclient
.idm_group_add_members("idm_admins", &["admin"])
.await
.unwrap();
// Create a new account
rsclient
.idm_account_create("demo_account", "Deeeeemo")
.await
.unwrap();
// Enroll a totp to the account
assert!(rsclient
.idm_account_primary_credential_set_password("demo_account", "sohdi3iuHo6mai7noh0a")
.await
.is_ok());
let (sessionid, tok) = rsclient
.idm_account_primary_credential_generate_totp("demo_account")
.await
.unwrap();
let r_tok: Totp = tok.into();
let totp = r_tok
.do_totp_duration_from_epoch(
&SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap(),
)
.expect("Failed to do totp?");
rsclient
.idm_account_primary_credential_verify_totp("demo_account", totp, sessionid)
.await
.unwrap(); // the result
// Check a good auth
let rsclient_good = rsclient.new_session().unwrap();
let totp = r_tok
.do_totp_duration_from_epoch(
&SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap(),
)
.expect("Failed to do totp?");
// TODO: It's extremely rare, but it's happened ONCE where, the time window
// elapsed DURING this test, so there is a minor possibility of this actually
// having a false negative. Is it possible to prevent this?
assert!(rsclient_good
.auth_password_totp("demo_account", "sohdi3iuHo6mai7noh0a", totp)
.await
.is_ok());
// Check a bad auth - needs to be second as we are going to trigger the slock.
// Get a new connection
let rsclient_bad = rsclient.new_session().unwrap();
assert!(rsclient_bad
.auth_password_totp("demo_account", "sohdi3iuHo6mai7noh0a", 0)
.await
.is_err());
// Delay by one second to allow the account to recover from the softlock.
std::thread::sleep(std::time::Duration::from_millis(1100));
// Remove TOTP on the account.
rsclient
.idm_account_primary_credential_remove_totp("demo_account")
.await
.unwrap();
// Check password auth.
let rsclient_good = rsclient.new_session().unwrap();
assert!(rsclient_good
.auth_simple_password("demo_account", "sohdi3iuHo6mai7noh0a")
.await
.is_ok());
}
#[tokio::test]
async fn test_server_rest_backup_code_auth_lifecycle() {
let rsclient = setup_async_test().await;
let res = rsclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
.await;
assert!(res.is_ok());
// Not recommended in production!
rsclient
.idm_group_add_members("idm_admins", &["admin"])
.await
.unwrap();
// Create a new account
rsclient
.idm_account_create("demo_account", "Deeeeemo")
.await
.unwrap();
// Enroll a totp to the account
assert!(rsclient
.idm_account_primary_credential_set_password("demo_account", "sohdi3iuHo6mai7noh0a")
.await
.is_ok());
let (sessionid, tok) = rsclient
.idm_account_primary_credential_generate_totp("demo_account")
.await
.unwrap();
let r_tok: Totp = tok.into();
let totp = r_tok
.do_totp_duration_from_epoch(
&SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap(),
)
.expect("Failed to do totp?");
rsclient
.idm_account_primary_credential_verify_totp("demo_account", totp, sessionid)
.await
.unwrap(); // the result
// Generate backup codes
let backup_codes = rsclient
.idm_account_primary_credential_generate_backup_code("demo_account")
.await
.expect("Failed to generate backup codes?");
// Check a good auth using a backup code
let rsclient_good = rsclient.new_session().unwrap();
assert!(rsclient_good
.auth_password_backup_code(
"demo_account",
"sohdi3iuHo6mai7noh0a",
backup_codes[0].as_str()
)
.await
.is_ok());
// Check a bad auth - needs to be second as we are going to trigger the slock.
// Get a new connection
let rsclient_bad = rsclient.new_session().unwrap();
assert!(rsclient_bad
.auth_password_backup_code("demo_account", "sohdi3iuHo6mai7noh0a", "wrong-backup-code")
.await
.is_err());
// Delay by one second to allow the account to recover from the softlock.
std::thread::sleep(std::time::Duration::from_millis(1100));
// Remove TOTP and backup codes on the account.
rsclient
.idm_account_primary_credential_remove_totp("demo_account")
.await
.unwrap();
// Check password auth.
let rsclient_good = rsclient.new_session().unwrap();
assert!(rsclient_good
.auth_simple_password("demo_account", "sohdi3iuHo6mai7noh0a")
.await
.is_ok());
}
#[tokio::test]
async fn test_server_rest_oauth2_basic_lifecycle() {
let rsclient = setup_async_test().await;
@ -1200,13 +915,13 @@ async fn test_server_credential_update_session_pw() {
// Create an account
rsclient
.idm_account_create("demo_account", "Demo Account")
.idm_person_account_create("demo_account", "Demo Account")
.await
.unwrap();
// Create an intent token for them
let intent_token = rsclient
.idm_account_credential_update_intent("demo_account")
.idm_person_account_credential_update_intent("demo_account")
.await
.unwrap();
@ -1257,20 +972,15 @@ async fn test_server_credential_update_session_totp_pw() {
.await
.unwrap();
// Create an account
// Create a person
// - person so they can self-modify credentials later in the test
rsclient
.idm_account_create("demo_account", "Demo Account")
.await
.unwrap();
// Make them a person so they can self-modify credentials later in the test
rsclient
.idm_account_person_extend("demo_account", None, None)
.idm_person_account_create("demo_account", "Demo Account")
.await
.unwrap();
let intent_token = rsclient
.idm_account_credential_update_intent("demo_account")
.idm_person_account_credential_update_intent("demo_account")
.await
.unwrap();
@ -1382,13 +1092,13 @@ async fn test_server_credential_update_session_passkey() {
// Create an account
rsclient
.idm_account_create("demo_account", "Demo Account")
.idm_person_account_create("demo_account", "Demo Account")
.await
.unwrap();
// Create an intent token for them
let intent_token = rsclient
.idm_account_credential_update_intent("demo_account")
.idm_person_account_credential_update_intent("demo_account")
.await
.unwrap();

View file

@ -24,12 +24,12 @@ use super::totpmodal::TotpModalApp;
use uuid::Uuid;
#[derive(PartialEq, Properties)]
#[derive(PartialEq, Eq, Properties)]
pub struct ModalProps {
pub token: CUSessionToken,
}
#[derive(PartialEq, Properties)]
#[derive(PartialEq, Eq, Properties)]
pub struct PasskeyRemoveModalProps {
pub token: CUSessionToken,
pub tag: String,

View file

@ -17,7 +17,7 @@ use crate::views::{ViewRoute, ViewsApp};
use serde::{Deserialize, Serialize};
// router to decide on state.
#[derive(Routable, PartialEq, Clone, Debug, Serialize, Deserialize)]
#[derive(Routable, PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
pub enum Route {
#[at("/")]
Landing,

View file

@ -31,7 +31,7 @@ pub fn clear_bearer_token() {
PersistentStorage::delete("kanidm_bearer_token");
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
pub enum Location {
Manager(Route),
Views(ViewRoute),

View file

@ -23,7 +23,7 @@ use apps::AppsApp;
use profile::ProfileApp;
use security::SecurityApp;
#[derive(Routable, PartialEq, Clone, Debug, Serialize, Deserialize)]
#[derive(Routable, PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
pub enum ViewRoute {
#[at("/ui/view/apps")]
Apps,
@ -46,7 +46,7 @@ enum State {
Error { emsg: String, kopid: Option<String> },
}
#[derive(PartialEq, Properties)]
#[derive(PartialEq, Eq, Properties)]
pub struct ViewProps {
pub token: String,
}

View file

@ -102,7 +102,7 @@ impl KaniHttpServer {
match e {
Entity::Account(a) => {
self.client
.idm_account_create(&a.name, &a.display_name)
.idm_person_account_create(&a.name, &a.display_name)
.await
.map(|_| ())
.or_else(|e| {
@ -125,7 +125,7 @@ impl KaniHttpServer {
// Now set the account password
self.client
.idm_account_primary_credential_set_password(&a.name, &a.password)
.idm_person_account_primary_credential_set_password(&a.name, &a.password)
.await
.map(|_| ())
.map_err(|e| {
@ -134,7 +134,7 @@ impl KaniHttpServer {
// For ldap tests, we need to make these posix accounts.
self.client
.idm_account_unix_extend(&a.name, None, None)
.idm_person_account_unix_extend(&a.name, None, None)
.await
.map(|_| ())
.map_err(|e| {
@ -142,7 +142,7 @@ impl KaniHttpServer {
})?;
self.client
.idm_account_unix_cred_put(&a.name, &a.password)
.idm_person_account_unix_cred_put(&a.name, &a.password)
.await
.map(|_| ())
.map_err(|e| {

View file

@ -105,7 +105,7 @@ impl LdapClient {
error!("Failed to initialise TLS -> {:?}", e);
})?;
let _ = SslStream::connect(Pin::new(&mut tlsstream))
SslStream::connect(Pin::new(&mut tlsstream))
.await
.map_err(|e| {
error!("Failed to initialise TLS -> {:?}", e);
@ -113,7 +113,7 @@ impl LdapClient {
let mut framed = Framed::new(tlsstream, LdapCodec);
let _ = framed.send(msg).await.map_err(|e| {
framed.send(msg).await.map_err(|e| {
error!("Unable to bind -> {:?}", e);
})?;
@ -223,7 +223,7 @@ impl LdapClient {
};
// Send it
let _ = inner.framed.send(msg).await.map_err(|e| {
inner.framed.send(msg).await.map_err(|e| {
error!("Unable to search -> {:?}", e);
})?;
@ -269,7 +269,7 @@ impl LdapClient {
};
// Send it
let _ = inner.framed.send(msg).await.map_err(|e| {
inner.framed.send(msg).await.map_err(|e| {
error!("Unable to delete -> {:?}", e);
})?;
if let Some(Ok(msg)) = inner.framed.next().await {
@ -306,7 +306,7 @@ impl LdapClient {
};
// Send it
let _ = inner.framed.send(msg).await.map_err(|e| {
inner.framed.send(msg).await.map_err(|e| {
error!("Unable to add -> {:?}", e);
})?;
if let Some(Ok(msg)) = inner.framed.next().await {
@ -343,7 +343,7 @@ impl LdapClient {
};
// Send it
let _ = inner.framed.send(msg).await.map_err(|e| {
inner.framed.send(msg).await.map_err(|e| {
error!("Unable to modify -> {:?}", e);
})?;
if let Some(Ok(msg)) = inner.framed.next().await {

View file

@ -14,5 +14,5 @@ tracing = { version = "^0.1.35", features = ["attributes", "max_level_trace", "r
tracing-subscriber = { version = "^0.3.14", features = ["env-filter"] }
# tracing-forest = { version = "0.1.4", features = ["uuid", "smallvec", "tokio", "env-filter"] }
tracing-forest = { git = "https://github.com/Firstyear/tracing-forest.git", rev = "18d242a4dde060c4946ade0a2c4d5be1df048aea", features = ["uuid", "smallvec", "tokio", "env-filter"] }
tracing-forest = { git = "https://github.com/QnnOkabayashi/tracing-forest.git", rev = "48d78f7294ceee47a22eee5c80964143c4fb3fe1", features = ["uuid", "smallvec", "tokio", "env-filter"] }