mirror of
https://github.com/nitnelave/lldap.git
synced 2023-04-12 14:25:13 +00:00
migration: Implement import from LDAP
This commit is contained in:
parent
aa83f6cab6
commit
31cf9b8e2c
253
Cargo.lock
generated
253
Cargo.lock
generated
@ -798,6 +798,31 @@ dependencies = [
|
|||||||
"lazy_static",
|
"lazy_static",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossterm"
|
||||||
|
version = "0.22.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c85525306c4291d1b73ce93c8acf9c339f9b213aef6c1d85c3830cbf1c16325c"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"crossterm_winapi",
|
||||||
|
"libc",
|
||||||
|
"mio",
|
||||||
|
"parking_lot",
|
||||||
|
"signal-hook",
|
||||||
|
"signal-hook-mio",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossterm_winapi"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-mac"
|
name = "crypto-mac"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
@ -1492,6 +1517,17 @@ dependencies = [
|
|||||||
"itoa",
|
"itoa",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http-body"
|
||||||
|
version = "0.4.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"http",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http-range"
|
name = "http-range"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
@ -1510,6 +1546,43 @@ version = "1.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440"
|
checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper"
|
||||||
|
version = "0.14.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "436ec0091e4f20e655156a30a0df3770fe2900aa301e548e08446ec794b6953c"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
|
"h2",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"httparse",
|
||||||
|
"httpdate",
|
||||||
|
"itoa",
|
||||||
|
"pin-project-lite",
|
||||||
|
"socket2",
|
||||||
|
"tokio",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
"want",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper-tls"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"hyper",
|
||||||
|
"native-tls",
|
||||||
|
"tokio",
|
||||||
|
"tokio-native-tls",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ident_case"
|
name = "ident_case"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
@ -1559,6 +1632,12 @@ dependencies = [
|
|||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ipnet"
|
||||||
|
version = "2.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
@ -1686,6 +1765,31 @@ dependencies = [
|
|||||||
"nom 2.2.1",
|
"nom 2.2.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ldap3"
|
||||||
|
version = "0.9.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b2bdad98cd197646a9fd7be985cb711cffaded69d8dc0d87d83f8d88bcbc1691"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"bytes",
|
||||||
|
"futures",
|
||||||
|
"futures-util",
|
||||||
|
"lazy_static",
|
||||||
|
"lber",
|
||||||
|
"log",
|
||||||
|
"maplit",
|
||||||
|
"native-tls",
|
||||||
|
"nom 2.2.1",
|
||||||
|
"percent-encoding",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
"tokio-native-tls",
|
||||||
|
"tokio-stream",
|
||||||
|
"tokio-util",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ldap3_server"
|
name = "ldap3_server"
|
||||||
version = "0.1.9"
|
version = "0.1.9"
|
||||||
@ -1767,7 +1871,7 @@ checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lldap"
|
name = "lldap"
|
||||||
version = "0.2.0"
|
version = "0.3.0-alpha.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix",
|
"actix",
|
||||||
"actix-files",
|
"actix-files",
|
||||||
@ -1823,7 +1927,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lldap_app"
|
name = "lldap_app"
|
||||||
version = "0.2.0"
|
version = "0.3.0-alpha.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
@ -1847,7 +1951,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lldap_auth"
|
name = "lldap_auth"
|
||||||
version = "0.2.0"
|
version = "0.3.0-alpha.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"curve25519-dalek",
|
"curve25519-dalek",
|
||||||
@ -1933,6 +2037,22 @@ version = "2.4.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
|
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "migration-tool"
|
||||||
|
version = "0.3.0-alpha.1"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"graphql_client",
|
||||||
|
"ldap3",
|
||||||
|
"lldap_auth",
|
||||||
|
"rand 0.8.4",
|
||||||
|
"requestty",
|
||||||
|
"reqwest",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mime"
|
name = "mime"
|
||||||
version = "0.3.16"
|
version = "0.3.16"
|
||||||
@ -2639,6 +2759,64 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "requestty"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "411059399ea4d5007971959900eee777750eaf539e4fdfecb9bb5d9b3fb99c40"
|
||||||
|
dependencies = [
|
||||||
|
"requestty-ui",
|
||||||
|
"smallvec",
|
||||||
|
"tempfile",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "requestty-ui"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f7f8e70d25cbc5d14d73c4f0c313ef505450a7c2a39b7e2ca421bc456a4574f6"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"crossterm",
|
||||||
|
"textwrap",
|
||||||
|
"unicode-segmentation",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "reqwest"
|
||||||
|
version = "0.11.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "07bea77bc708afa10e59905c3d4af7c8fd43c9214251673095ff8b14345fcbc5"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"bytes",
|
||||||
|
"encoding_rs",
|
||||||
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"hyper",
|
||||||
|
"hyper-tls",
|
||||||
|
"ipnet",
|
||||||
|
"js-sys",
|
||||||
|
"lazy_static",
|
||||||
|
"log",
|
||||||
|
"mime",
|
||||||
|
"native-tls",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_urlencoded",
|
||||||
|
"tokio",
|
||||||
|
"tokio-native-tls",
|
||||||
|
"url",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
"winreg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rsa"
|
name = "rsa"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
@ -2901,6 +3079,27 @@ dependencies = [
|
|||||||
"lazy_static",
|
"lazy_static",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook"
|
||||||
|
version = "0.3.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9c98891d737e271a2954825ef19e46bd16bdb98e2746f2eec4f7a4ef7946efd1"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"signal-hook-registry",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook-mio"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29fd5867f1c4f2c5be079aee7a2adf1152ebb04a4bc4d341f504b7dece607ed4"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"mio",
|
||||||
|
"signal-hook",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook-registry"
|
name = "signal-hook-registry"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
@ -2942,6 +3141,12 @@ dependencies = [
|
|||||||
"static_assertions",
|
"static_assertions",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smawk"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@ -3209,6 +3414,8 @@ version = "0.14.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80"
|
checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"smawk",
|
||||||
|
"unicode-linebreak",
|
||||||
"unicode-width",
|
"unicode-width",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -3379,6 +3586,12 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower-service"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing"
|
name = "tracing"
|
||||||
version = "0.1.26"
|
version = "0.1.26"
|
||||||
@ -3465,6 +3678,12 @@ version = "0.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41"
|
checksum = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "try-lock"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.14.0"
|
version = "1.14.0"
|
||||||
@ -3501,6 +3720,15 @@ version = "0.3.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085"
|
checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-linebreak"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3a52dcaab0c48d931f7cc8ef826fa51690a08e1ea55117ef26f89864f532383f"
|
||||||
|
dependencies = [
|
||||||
|
"regex",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-normalization"
|
name = "unicode-normalization"
|
||||||
version = "0.1.19"
|
version = "0.1.19"
|
||||||
@ -3630,6 +3858,16 @@ version = "1.0.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
|
checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "want"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"try-lock",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.9.0+wasi-snapshot-preview1"
|
version = "0.9.0+wasi-snapshot-preview1"
|
||||||
@ -3761,6 +3999,15 @@ version = "0.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winreg"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wyz"
|
name = "wyz"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
@ -2,9 +2,12 @@
|
|||||||
members = [
|
members = [
|
||||||
"server",
|
"server",
|
||||||
"auth",
|
"auth",
|
||||||
"app"
|
"app",
|
||||||
|
"migration-tool"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
default-members = ["server"]
|
||||||
|
|
||||||
# TODO: remove when there's a new release.
|
# TODO: remove when there's a new release.
|
||||||
[patch.crates-io.yew_form]
|
[patch.crates-io.yew_form]
|
||||||
git = 'https://github.com/sassman/yew_form/'
|
git = 'https://github.com/sassman/yew_form/'
|
||||||
|
23
migration-tool/Cargo.toml
Normal file
23
migration-tool/Cargo.toml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
[package]
|
||||||
|
name = "migration-tool"
|
||||||
|
version = "0.3.0-alpha.1"
|
||||||
|
edition = "2021"
|
||||||
|
authors = ["Valentin Tolmer <valentin@tolmer.fr>"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "*"
|
||||||
|
graphql_client = "0.10"
|
||||||
|
ldap3 = "*"
|
||||||
|
rand = "0.8"
|
||||||
|
requestty = "*"
|
||||||
|
serde = "1"
|
||||||
|
serde_json = "1"
|
||||||
|
smallvec = "*"
|
||||||
|
|
||||||
|
[dependencies.lldap_auth]
|
||||||
|
path = "../auth"
|
||||||
|
features = [ "opaque_client" ]
|
||||||
|
|
||||||
|
[dependencies.reqwest]
|
||||||
|
version = "*"
|
||||||
|
features = [ "json", "blocking" ]
|
5
migration-tool/queries/add_user_to_group.graphql
Normal file
5
migration-tool/queries/add_user_to_group.graphql
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
mutation AddUserToGroup($user: String!, $group: Int!) {
|
||||||
|
addUserToGroup(userId: $user, groupId: $group) {
|
||||||
|
ok
|
||||||
|
}
|
||||||
|
}
|
6
migration-tool/queries/create_group.graphql
Normal file
6
migration-tool/queries/create_group.graphql
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
mutation CreateGroup($name: String!) {
|
||||||
|
createGroup(name: $name) {
|
||||||
|
id
|
||||||
|
displayName
|
||||||
|
}
|
||||||
|
}
|
5
migration-tool/queries/create_user.graphql
Normal file
5
migration-tool/queries/create_user.graphql
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
mutation CreateUser($user: CreateUserInput!) {
|
||||||
|
createUser(user: $user) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
9
migration-tool/queries/list_groups.graphql
Normal file
9
migration-tool/queries/list_groups.graphql
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
query ListGroups {
|
||||||
|
groups {
|
||||||
|
id
|
||||||
|
displayName
|
||||||
|
users {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
5
migration-tool/queries/list_users.graphql
Normal file
5
migration-tool/queries/list_users.graphql
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
query ListUsers {
|
||||||
|
users(filters: null) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
432
migration-tool/src/ldap.rs
Normal file
432
migration-tool/src/ldap.rs
Normal file
@ -0,0 +1,432 @@
|
|||||||
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use ldap3::{ResultEntry, SearchEntry};
|
||||||
|
use requestty::{prompt_one, Question};
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
|
use crate::lldap::User;
|
||||||
|
|
||||||
|
pub struct LdapClient {
|
||||||
|
domain: String,
|
||||||
|
connection: ldap3::LdapConn,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if the URL starts with the protocol, and whether the host is valid (DNS and listening),
|
||||||
|
/// potentially with the given port. Returns the address + port that managed to connect, if any.
|
||||||
|
pub fn check_host_exists(
|
||||||
|
url: &str,
|
||||||
|
protocol_and_port: &[(&str, u16)],
|
||||||
|
) -> std::result::Result<Option<String>, String> {
|
||||||
|
for (protocol, port) in protocol_and_port {
|
||||||
|
if url.starts_with(protocol) {
|
||||||
|
use std::net::ToSocketAddrs;
|
||||||
|
let trimmed_url = url.trim_start_matches(protocol);
|
||||||
|
return match trimmed_url.to_socket_addrs() {
|
||||||
|
Ok(_) => Ok(Some(url.to_owned())),
|
||||||
|
Err(_) => {
|
||||||
|
let new_url = format!("{}:{}", trimmed_url, port);
|
||||||
|
new_url
|
||||||
|
.to_socket_addrs()
|
||||||
|
.map_err(|_| format!("Could not resolve host: '{}'", trimmed_url))
|
||||||
|
.map(|_| Some(format!("{}{}", protocol, new_url)))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn autocomplete_domain_suffix(input: String, domain: &str) -> SmallVec<[String; 1]> {
|
||||||
|
let mut answers = SmallVec::<[String; 1]>::new();
|
||||||
|
for part in input.split(',') {
|
||||||
|
if !part.starts_with('d') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if domain.starts_with(part) {
|
||||||
|
answers.push(input.clone() + domain.trim_start_matches(part));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
answers.push(input);
|
||||||
|
answers
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Asks the user for the URL of the LDAP server, and checks that a connection can be established.
|
||||||
|
/// Returns the LDAP URL.
|
||||||
|
fn get_ldap_url() -> Result<String> {
|
||||||
|
let ldap_protocols = &[("ldap://", 389), ("ldaps://", 636)];
|
||||||
|
let question = Question::input("ldap_url")
|
||||||
|
.message("LDAP_URL (ldap://...)")
|
||||||
|
.auto_complete(|answer, _| {
|
||||||
|
let mut answers = SmallVec::<[String; 1]>::new();
|
||||||
|
if "ldap://".starts_with(&answer) {
|
||||||
|
answers.push("ldap://".to_owned());
|
||||||
|
}
|
||||||
|
if "ldaps://".starts_with(&answer) {
|
||||||
|
answers.push("ldaps://".to_owned());
|
||||||
|
}
|
||||||
|
answers.push(answer);
|
||||||
|
answers
|
||||||
|
})
|
||||||
|
.validate(|url, _| {
|
||||||
|
if let Some(url) = check_host_exists(url, ldap_protocols)? {
|
||||||
|
ldap3::LdapConn::new(&url)
|
||||||
|
.map_err(|e| format!("Could not connect to LDAP server: {}", e))?;
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err("LDAP URL should start with 'ldap://' or 'ldaps://'".to_owned())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
let answer = prompt_one(question)?;
|
||||||
|
Ok(
|
||||||
|
check_host_exists(answer.as_string().unwrap(), ldap_protocols)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Binds the LDAP connection by asking the user for the bind DN and password, and returns the bind
|
||||||
|
/// DN.
|
||||||
|
fn bind_ldap(
|
||||||
|
ldap_connection: &mut ldap3::LdapConn,
|
||||||
|
previous_binddn: Option<String>,
|
||||||
|
) -> Result<String> {
|
||||||
|
let binddn = {
|
||||||
|
let question = Question::input("ldap_binddn")
|
||||||
|
.message("LDAP_BIND_DN (cn=...)")
|
||||||
|
.validate(|dn, _| {
|
||||||
|
if dn.contains(',') && dn.contains('=') {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(
|
||||||
|
"Invalid bind DN, expected something like 'cn=admin,dc=example,dc=com'"
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.auto_complete(|answer, _| {
|
||||||
|
let mut answers = SmallVec::<[String; 1]>::new();
|
||||||
|
if let Some(binddn) = &previous_binddn {
|
||||||
|
answers.push(binddn.clone());
|
||||||
|
}
|
||||||
|
answers.push(answer);
|
||||||
|
answers
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
let answer = prompt_one(question)?;
|
||||||
|
answer.as_string().unwrap().to_owned()
|
||||||
|
};
|
||||||
|
let password = {
|
||||||
|
let question = Question::password("ldap_bind_password")
|
||||||
|
.message("LDAP_BIND_PASSWORD")
|
||||||
|
.validate(|password, _| {
|
||||||
|
if !password.is_empty() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err("Empty password".to_owned())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
let answer = prompt_one(question)?;
|
||||||
|
answer.as_string().unwrap().to_owned()
|
||||||
|
};
|
||||||
|
if let Err(e) = ldap_connection
|
||||||
|
.simple_bind(&binddn, &password)
|
||||||
|
.and_then(|r| r.success())
|
||||||
|
{
|
||||||
|
println!("Error connecting as '{}': {}", binddn, e);
|
||||||
|
bind_ldap(ldap_connection, Some(binddn))
|
||||||
|
} else {
|
||||||
|
Ok(binddn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<ResultEntry> for User {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
fn try_from(value: ResultEntry) -> Result<Self> {
|
||||||
|
let entry = SearchEntry::construct(value);
|
||||||
|
let get_required_attribute = |attr| {
|
||||||
|
entry
|
||||||
|
.attrs
|
||||||
|
.get(attr)
|
||||||
|
.ok_or_else(|| anyhow!("Missing {} for user", attr))
|
||||||
|
.and_then(|u| {
|
||||||
|
if u.len() > 1 {
|
||||||
|
Err(anyhow!("Too many {}s", attr))
|
||||||
|
} else {
|
||||||
|
Ok(u.first().unwrap().to_owned())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let id = get_required_attribute("uid")
|
||||||
|
.or_else(|_| get_required_attribute("sAMAccountName"))
|
||||||
|
.or_else(|_| get_required_attribute("userPrincipalName"))?;
|
||||||
|
let email = get_required_attribute("mail")
|
||||||
|
.or_else(|_| get_required_attribute("rfc822mailbox"))
|
||||||
|
.context(format!("for user '{}'", id))?;
|
||||||
|
|
||||||
|
let get_optional_attribute = |attr: &str| {
|
||||||
|
entry
|
||||||
|
.attrs
|
||||||
|
.get(attr)
|
||||||
|
.and_then(|v| v.first().map(|s| s.as_str()))
|
||||||
|
.and_then(|s| {
|
||||||
|
if s.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(s.to_owned())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let last_name = get_optional_attribute("sn").or_else(|| get_optional_attribute("surname"));
|
||||||
|
let display_name = get_optional_attribute("cn")
|
||||||
|
.or_else(|| get_optional_attribute("commonName"))
|
||||||
|
.or_else(|| get_optional_attribute("name"))
|
||||||
|
.or_else(|| get_optional_attribute("displayName"));
|
||||||
|
let first_name = get_optional_attribute("givenName");
|
||||||
|
let password =
|
||||||
|
get_optional_attribute("userPassword").or_else(|| get_optional_attribute("password"));
|
||||||
|
Ok(User::new(
|
||||||
|
id,
|
||||||
|
email,
|
||||||
|
display_name,
|
||||||
|
first_name,
|
||||||
|
last_name,
|
||||||
|
password,
|
||||||
|
entry.dn,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OuType {
|
||||||
|
User,
|
||||||
|
Group,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detect_ou(
|
||||||
|
ldap_connection: &mut ldap3::LdapConn,
|
||||||
|
domain: &str,
|
||||||
|
for_type: OuType,
|
||||||
|
) -> Result<(Option<String>, Vec<String>), anyhow::Error> {
|
||||||
|
let ous = ldap_connection
|
||||||
|
.search(
|
||||||
|
domain,
|
||||||
|
ldap3::Scope::Subtree,
|
||||||
|
"(objectClass=organizationalUnit)",
|
||||||
|
vec!["dn"],
|
||||||
|
)?
|
||||||
|
.success()?
|
||||||
|
.0;
|
||||||
|
let mut detected_ou = None;
|
||||||
|
let mut all_ous = Vec::new();
|
||||||
|
for result_entry in ous {
|
||||||
|
let dn = SearchEntry::construct(result_entry).dn;
|
||||||
|
match for_type {
|
||||||
|
OuType::User => {
|
||||||
|
if dn.contains("user") || dn.contains("people") || dn.contains("person") {
|
||||||
|
detected_ou = Some(dn.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OuType::Group => {
|
||||||
|
if dn.contains("group") {
|
||||||
|
detected_ou = Some(dn.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
all_ous.push(dn);
|
||||||
|
}
|
||||||
|
Ok((detected_ou, all_ous))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_users(connection: &mut LdapClient) -> Result<Vec<User>, anyhow::Error> {
|
||||||
|
let LdapClient {
|
||||||
|
connection: ldap_connection,
|
||||||
|
domain,
|
||||||
|
} = connection;
|
||||||
|
let domain = domain.as_str();
|
||||||
|
let (maybe_user_ou, all_ous) = detect_ou(ldap_connection, domain, OuType::User)?;
|
||||||
|
let user_ou = {
|
||||||
|
let question = Question::input("ldap_user_ou")
|
||||||
|
.message(format!(
|
||||||
|
"Where are the users located (under '{}')? {}(LDAP_USERS_DN)",
|
||||||
|
domain,
|
||||||
|
maybe_user_ou
|
||||||
|
.as_ref()
|
||||||
|
.map(|ou| format!("Detected: {}", ou))
|
||||||
|
.unwrap_or_default()
|
||||||
|
))
|
||||||
|
.validate(|dn, _| {
|
||||||
|
if dn.contains('=') {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(format!(
|
||||||
|
"Invalid bind DN, expected something like 'ou=people,{}'",
|
||||||
|
domain
|
||||||
|
))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.default(maybe_user_ou.unwrap_or_default())
|
||||||
|
.auto_complete(|s, _| {
|
||||||
|
let mut answers = autocomplete_domain_suffix(s, domain);
|
||||||
|
answers.extend(all_ous.clone().into_iter());
|
||||||
|
answers
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
let answer = prompt_one(question)?;
|
||||||
|
let mut answer = answer.as_string().unwrap().to_owned();
|
||||||
|
if !answer.ends_with(domain) {
|
||||||
|
if !answer.is_empty() {
|
||||||
|
answer += ",";
|
||||||
|
}
|
||||||
|
answer += domain;
|
||||||
|
}
|
||||||
|
answer
|
||||||
|
};
|
||||||
|
let users = ldap_connection
|
||||||
|
.search(
|
||||||
|
&user_ou,
|
||||||
|
ldap3::Scope::Subtree,
|
||||||
|
"(|(objectClass=inetOrgPerson)(objectClass=person)(objectClass=mailAccount)(objectClass=posixAccount)(objectClass=user)(objectClass=organizationalPerson))",
|
||||||
|
vec![
|
||||||
|
"uid",
|
||||||
|
"sAMAccountName",
|
||||||
|
"userPrincipalName",
|
||||||
|
"mail",
|
||||||
|
"rfc822mailbox",
|
||||||
|
"givenName",
|
||||||
|
"sn",
|
||||||
|
"surname",
|
||||||
|
"cn",
|
||||||
|
"commonName",
|
||||||
|
"displayName",
|
||||||
|
"name",
|
||||||
|
"userPassword",
|
||||||
|
],
|
||||||
|
)?
|
||||||
|
.success()?
|
||||||
|
.0;
|
||||||
|
users
|
||||||
|
.into_iter()
|
||||||
|
.map(TryFrom::try_from)
|
||||||
|
.collect::<Result<Vec<User>>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct LdapGroup {
|
||||||
|
pub name: String,
|
||||||
|
pub members: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<ResultEntry> for LdapGroup {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
// https://github.com/graphql-rust/graphql-client/issues/386
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
fn try_from(value: ResultEntry) -> Result<Self> {
|
||||||
|
let entry = SearchEntry::construct(value);
|
||||||
|
let get_required_attribute = |attr| {
|
||||||
|
entry
|
||||||
|
.attrs
|
||||||
|
.get(attr)
|
||||||
|
.ok_or_else(|| anyhow!("Missing {} for user", attr))
|
||||||
|
.and_then(|u| {
|
||||||
|
if u.len() > 1 {
|
||||||
|
Err(anyhow!("Too many {}s", attr))
|
||||||
|
} else {
|
||||||
|
Ok(u.first().unwrap().to_owned())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let name = get_required_attribute("cn")
|
||||||
|
.or_else(|_| get_required_attribute("commonName"))
|
||||||
|
.or_else(|_| get_required_attribute("displayName"))
|
||||||
|
.or_else(|_| get_required_attribute("name"))?;
|
||||||
|
|
||||||
|
let get_repeated_attribute = |attr: &str| entry.attrs.get(attr).map(|v| v.to_owned());
|
||||||
|
let members = get_repeated_attribute("member")
|
||||||
|
.or_else(|| get_repeated_attribute("uniqueMember"))
|
||||||
|
.unwrap_or_default();
|
||||||
|
Ok(LdapGroup { name, members })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_groups(connection: &mut LdapClient) -> Result<Vec<LdapGroup>> {
|
||||||
|
let LdapClient {
|
||||||
|
connection: ldap_connection,
|
||||||
|
domain,
|
||||||
|
} = connection;
|
||||||
|
let domain = domain.as_str();
|
||||||
|
let (maybe_group_ou, all_ous) = detect_ou(ldap_connection, domain, OuType::Group)?;
|
||||||
|
let group_ou = {
|
||||||
|
let question = Question::input("ldap_group_ou")
|
||||||
|
.message(format!(
|
||||||
|
"Where are the groups located (under '{}')? {}(LDAP_GROUPS_DN)",
|
||||||
|
domain,
|
||||||
|
maybe_group_ou
|
||||||
|
.as_ref()
|
||||||
|
.map(|ou| format!("Detected: {}", ou))
|
||||||
|
.unwrap_or_default()
|
||||||
|
))
|
||||||
|
.validate(|dn, _| {
|
||||||
|
if dn.contains('=') {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(format!(
|
||||||
|
"Invalid bind DN, expected something like 'ou=groups,{}'",
|
||||||
|
domain
|
||||||
|
))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.default(maybe_group_ou.unwrap_or_default())
|
||||||
|
.auto_complete(|s, _| {
|
||||||
|
let mut answers = autocomplete_domain_suffix(s, domain);
|
||||||
|
answers.extend(all_ous.clone().into_iter());
|
||||||
|
answers
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
let answer = prompt_one(question)?;
|
||||||
|
let mut answer = answer.as_string().unwrap().to_owned();
|
||||||
|
if !answer.ends_with(domain) {
|
||||||
|
if !answer.is_empty() {
|
||||||
|
answer += ",";
|
||||||
|
}
|
||||||
|
answer += domain;
|
||||||
|
}
|
||||||
|
answer
|
||||||
|
};
|
||||||
|
let groups = ldap_connection
|
||||||
|
.search(
|
||||||
|
&group_ou,
|
||||||
|
ldap3::Scope::Subtree,
|
||||||
|
"(|(objectClass=group)(objectClass=groupOfNames)(objectClass=groupOfUniqueNames))",
|
||||||
|
vec![
|
||||||
|
"cn",
|
||||||
|
"commonName",
|
||||||
|
"displayName",
|
||||||
|
"name",
|
||||||
|
"member",
|
||||||
|
"uniqueMember",
|
||||||
|
],
|
||||||
|
)?
|
||||||
|
.success()?
|
||||||
|
.0;
|
||||||
|
let input_groups = groups
|
||||||
|
.into_iter()
|
||||||
|
.map(TryFrom::try_from)
|
||||||
|
.collect::<Result<Vec<LdapGroup>>>()?;
|
||||||
|
Ok(input_groups)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_ldap_connection() -> Result<LdapClient, anyhow::Error> {
|
||||||
|
let url = get_ldap_url()?;
|
||||||
|
let mut ldap_connection = ldap3::LdapConn::new(&url)?;
|
||||||
|
println!("Server found");
|
||||||
|
let bind_dn = bind_ldap(&mut ldap_connection, None)?;
|
||||||
|
println!("Connection established");
|
||||||
|
let domain = &bind_dn[(bind_dn.find(",dc=").expect("Could not find domain?!") + 1)..];
|
||||||
|
// domain is 'dc=example,dc=com'
|
||||||
|
Ok(LdapClient {
|
||||||
|
connection: ldap_connection,
|
||||||
|
domain: domain.to_owned(),
|
||||||
|
})
|
||||||
|
}
|
506
migration-tool/src/lldap.rs
Normal file
506
migration-tool/src/lldap.rs
Normal file
@ -0,0 +1,506 @@
|
|||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
|
use graphql_client::GraphQLQuery;
|
||||||
|
use requestty::{prompt_one, Question};
|
||||||
|
use reqwest::blocking::{Client, ClientBuilder};
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
|
use crate::ldap::{check_host_exists, LdapGroup};
|
||||||
|
|
||||||
|
pub struct GraphQLClient {
|
||||||
|
url: String,
|
||||||
|
auth_header: reqwest::header::HeaderValue,
|
||||||
|
client: Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GraphQLClient {
|
||||||
|
fn new(url: String, auth_token: &str, client: Client) -> Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
url: format!("{}/api/graphql", url),
|
||||||
|
auth_header: format!("Bearer {}", auth_token).parse()?,
|
||||||
|
client,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn post<QueryType>(
|
||||||
|
&self,
|
||||||
|
variables: QueryType::Variables,
|
||||||
|
) -> Result<QueryType::ResponseData>
|
||||||
|
where
|
||||||
|
QueryType: GraphQLQuery + 'static,
|
||||||
|
{
|
||||||
|
let unwrap_graphql_response = |graphql_client::Response { data, errors }| {
|
||||||
|
data.ok_or_else(|| {
|
||||||
|
anyhow!(
|
||||||
|
"Errors: [{}]",
|
||||||
|
errors
|
||||||
|
.unwrap_or_default()
|
||||||
|
.iter()
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
|
)
|
||||||
|
})
|
||||||
|
};
|
||||||
|
self.client
|
||||||
|
.post(&self.url)
|
||||||
|
.header(reqwest::header::AUTHORIZATION, &self.auth_header)
|
||||||
|
// Request body.
|
||||||
|
.json(&QueryType::build_query(variables))
|
||||||
|
.send()
|
||||||
|
.context("while sending a request to the LLDAP server")?
|
||||||
|
.error_for_status()
|
||||||
|
.context("error from an LLDAP response")?
|
||||||
|
// Parse response as Json.
|
||||||
|
.json::<graphql_client::Response<QueryType::ResponseData>>()
|
||||||
|
.context("while parsing backend response")
|
||||||
|
.and_then(unwrap_graphql_response)
|
||||||
|
.context("GraphQL error from an LLDAP response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct User {
|
||||||
|
pub user_input: create_user::CreateUserInput,
|
||||||
|
pub password: Option<String>,
|
||||||
|
pub dn: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl User {
|
||||||
|
// https://github.com/graphql-rust/graphql-client/issues/386
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn new(
|
||||||
|
id: String,
|
||||||
|
email: String,
|
||||||
|
displayName: Option<String>,
|
||||||
|
firstName: Option<String>,
|
||||||
|
lastName: Option<String>,
|
||||||
|
password: Option<String>,
|
||||||
|
dn: String,
|
||||||
|
) -> User {
|
||||||
|
User {
|
||||||
|
user_input: create_user::CreateUserInput {
|
||||||
|
id,
|
||||||
|
email,
|
||||||
|
displayName,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
},
|
||||||
|
password,
|
||||||
|
dn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(GraphQLQuery)]
|
||||||
|
#[graphql(
|
||||||
|
schema_path = "../schema.graphql",
|
||||||
|
query_path = "queries/create_user.graphql",
|
||||||
|
response_derives = "Debug",
|
||||||
|
variables_derives = "Debug,Clone",
|
||||||
|
custom_scalars_module = "crate::infra::graphql"
|
||||||
|
)]
|
||||||
|
struct CreateUser;
|
||||||
|
|
||||||
|
#[derive(GraphQLQuery)]
|
||||||
|
#[graphql(
|
||||||
|
schema_path = "../schema.graphql",
|
||||||
|
query_path = "queries/create_group.graphql",
|
||||||
|
response_derives = "Debug",
|
||||||
|
variables_derives = "Debug,Clone",
|
||||||
|
custom_scalars_module = "crate::infra::graphql"
|
||||||
|
)]
|
||||||
|
struct CreateGroup;
|
||||||
|
|
||||||
|
#[derive(GraphQLQuery)]
|
||||||
|
#[graphql(
|
||||||
|
schema_path = "../schema.graphql",
|
||||||
|
query_path = "queries/list_users.graphql",
|
||||||
|
response_derives = "Debug",
|
||||||
|
custom_scalars_module = "crate::infra::graphql"
|
||||||
|
)]
|
||||||
|
struct ListUsers;
|
||||||
|
|
||||||
|
#[derive(GraphQLQuery)]
|
||||||
|
#[graphql(
|
||||||
|
schema_path = "../schema.graphql",
|
||||||
|
query_path = "queries/list_groups.graphql",
|
||||||
|
response_derives = "Debug",
|
||||||
|
custom_scalars_module = "crate::infra::graphql"
|
||||||
|
)]
|
||||||
|
struct ListGroups;
|
||||||
|
|
||||||
|
pub type LldapGroup = list_groups::ListGroupsGroups;
|
||||||
|
|
||||||
|
fn try_login(
|
||||||
|
lldap_server: &str,
|
||||||
|
username: &str,
|
||||||
|
password: &str,
|
||||||
|
client: &Client,
|
||||||
|
) -> Result<String> {
|
||||||
|
let mut rng = rand::rngs::OsRng;
|
||||||
|
use lldap_auth::login::*;
|
||||||
|
use lldap_auth::opaque::client::login::*;
|
||||||
|
let ClientLoginStartResult { state, message } =
|
||||||
|
start_login(password, &mut rng).context("Could not initialize login")?;
|
||||||
|
let req = ClientLoginStartRequest {
|
||||||
|
username: username.to_owned(),
|
||||||
|
login_start_request: message,
|
||||||
|
};
|
||||||
|
let response = client
|
||||||
|
.post(format!("{}/auth/opaque/login/start", lldap_server))
|
||||||
|
.json(&req)
|
||||||
|
.send()
|
||||||
|
.context("while trying to login to LLDAP")?;
|
||||||
|
if !response.status().is_success() {
|
||||||
|
bail!(
|
||||||
|
"Failed to start logging in to LLDAP: {}",
|
||||||
|
response.status().as_str()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let login_start_response = response.json::<lldap_auth::login::ServerLoginStartResponse>()?;
|
||||||
|
let login_finish = finish_login(state, login_start_response.credential_response)?;
|
||||||
|
let req = ClientLoginFinishRequest {
|
||||||
|
server_data: login_start_response.server_data,
|
||||||
|
credential_finalization: login_finish.message,
|
||||||
|
};
|
||||||
|
let response = client
|
||||||
|
.post(format!("{}/auth/opaque/login/finish", lldap_server))
|
||||||
|
.json(&req)
|
||||||
|
.send()?;
|
||||||
|
if !response.status().is_success() {
|
||||||
|
bail!(
|
||||||
|
"Failed to finish logging in to LLDAP: {}",
|
||||||
|
response.status().as_str()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(response.text()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_lldap_user_and_password(
|
||||||
|
lldap_server: &str,
|
||||||
|
client: &Client,
|
||||||
|
previous_username: Option<String>,
|
||||||
|
) -> Result<String> {
|
||||||
|
let username = {
|
||||||
|
let question = Question::input("lldap_username")
|
||||||
|
.message("LLDAP_USERNAME (default=admin)")
|
||||||
|
.default("admin")
|
||||||
|
.auto_complete(|answer, _| {
|
||||||
|
let mut answers = SmallVec::<[String; 1]>::new();
|
||||||
|
if let Some(username) = &previous_username {
|
||||||
|
answers.push(username.clone());
|
||||||
|
}
|
||||||
|
answers.push(answer);
|
||||||
|
answers
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
let answer = prompt_one(question)?;
|
||||||
|
answer.as_string().unwrap().to_owned()
|
||||||
|
};
|
||||||
|
let password = {
|
||||||
|
let question = Question::password("lldap_password")
|
||||||
|
.message("LLDAP_PASSWORD")
|
||||||
|
.validate(|password, _| {
|
||||||
|
if !password.is_empty() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err("Empty password".to_owned())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
let answer = prompt_one(question)?;
|
||||||
|
answer.as_string().unwrap().to_owned()
|
||||||
|
};
|
||||||
|
match try_login(lldap_server, &username, &password, client) {
|
||||||
|
Err(e) => {
|
||||||
|
println!("Could not login: {:#?}", e);
|
||||||
|
get_lldap_user_and_password(lldap_server, client, Some(username))
|
||||||
|
}
|
||||||
|
Ok(token) => Ok(token),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_lldap_client() -> Result<GraphQLClient> {
|
||||||
|
let client = ClientBuilder::new()
|
||||||
|
.connect_timeout(std::time::Duration::from_secs(2))
|
||||||
|
.timeout(std::time::Duration::from_secs(5))
|
||||||
|
.redirect(reqwest::redirect::Policy::none())
|
||||||
|
.build()?;
|
||||||
|
let lldap_server = get_lldap_server(&client)?;
|
||||||
|
let token = get_lldap_user_and_password(&lldap_server, &client, None)?;
|
||||||
|
println!("Successfully connected to LLDAP");
|
||||||
|
GraphQLClient::new(lldap_server, &token, client)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_users_into_lldap(
|
||||||
|
users: Vec<User>,
|
||||||
|
existing_users: &mut Vec<String>,
|
||||||
|
graphql_client: &GraphQLClient,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut added_users_count = 0;
|
||||||
|
let mut skip_all = false;
|
||||||
|
for user in users {
|
||||||
|
let uid = user.user_input.id.clone();
|
||||||
|
loop {
|
||||||
|
print!("Adding {}... ", &uid);
|
||||||
|
match graphql_client
|
||||||
|
.post::<CreateUser>(create_user::Variables {
|
||||||
|
user: user.user_input.clone(),
|
||||||
|
})
|
||||||
|
.context(format!("while creating user '{}'", uid))
|
||||||
|
{
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error: {:#?}", e);
|
||||||
|
if skip_all {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let question = requestty::Question::select("skip_user")
|
||||||
|
.message(format!("Error while adding user {}", &uid))
|
||||||
|
.choices(vec!["Skip", "Retry", "Skip all"])
|
||||||
|
.default_separator()
|
||||||
|
.choice("Abort")
|
||||||
|
.build();
|
||||||
|
let answer = prompt_one(question)?;
|
||||||
|
let choice = answer.as_list_item().unwrap();
|
||||||
|
match choice.text.as_str() {
|
||||||
|
"Skip" => break,
|
||||||
|
"Retry" => continue,
|
||||||
|
"Skip all" => {
|
||||||
|
skip_all = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
"Abort" => return Err(e),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(response) => {
|
||||||
|
println!("Done!");
|
||||||
|
added_users_count += 1;
|
||||||
|
existing_users.push(response.create_user.id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("{} users successfully added", added_users_count);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_groups_into_lldap(
|
||||||
|
groups: &[LdapGroup],
|
||||||
|
lldap_groups: &mut Vec<LldapGroup>,
|
||||||
|
graphql_client: &GraphQLClient,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut added_groups_count = 0;
|
||||||
|
let mut skip_all = false;
|
||||||
|
let existing_group_names =
|
||||||
|
HashSet::<&str>::from_iter(lldap_groups.iter().map(|g| g.display_name.as_str()));
|
||||||
|
let new_groups = groups
|
||||||
|
.iter()
|
||||||
|
.filter(|g| !existing_group_names.contains(g.name.as_str()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for group in new_groups {
|
||||||
|
let name = group.name.clone();
|
||||||
|
loop {
|
||||||
|
print!("Adding {}... ", &name);
|
||||||
|
match graphql_client
|
||||||
|
.post::<CreateGroup>(create_group::Variables { name: name.clone() })
|
||||||
|
.context(format!("while creating group '{}'", &name))
|
||||||
|
{
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error: {:#?}", e);
|
||||||
|
if skip_all {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let question = requestty::Question::select("skip_group")
|
||||||
|
.message(format!("Error while adding group {}", &name))
|
||||||
|
.choices(vec!["Skip", "Retry", "Skip all"])
|
||||||
|
.default_separator()
|
||||||
|
.choice("Abort")
|
||||||
|
.build();
|
||||||
|
let answer = prompt_one(question)?;
|
||||||
|
let choice = answer.as_list_item().unwrap();
|
||||||
|
match choice.text.as_str() {
|
||||||
|
"Skip" => break,
|
||||||
|
"Retry" => continue,
|
||||||
|
"Skip all" => {
|
||||||
|
skip_all = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
"Abort" => return Err(e),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(response) => {
|
||||||
|
println!("Done!");
|
||||||
|
added_groups_count += 1;
|
||||||
|
lldap_groups.push(LldapGroup {
|
||||||
|
id: response.create_group.id,
|
||||||
|
display_name: group.name.clone(),
|
||||||
|
users: Vec::new(),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("{} groups successfully added", added_groups_count);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_lldap_users(graphql_client: &GraphQLClient) -> Result<Vec<String>> {
|
||||||
|
Ok(graphql_client
|
||||||
|
.post::<ListUsers>(list_users::Variables {})?
|
||||||
|
.users
|
||||||
|
.into_iter()
|
||||||
|
.map(|u| u.id)
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_lldap_groups(graphql_client: &GraphQLClient) -> Result<Vec<LldapGroup>> {
|
||||||
|
Ok(graphql_client
|
||||||
|
.post::<ListGroups>(list_groups::Variables {})?
|
||||||
|
.groups)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(GraphQLQuery)]
|
||||||
|
#[graphql(
|
||||||
|
schema_path = "../schema.graphql",
|
||||||
|
query_path = "queries/add_user_to_group.graphql",
|
||||||
|
response_derives = "Debug",
|
||||||
|
variables_derives = "Debug,Clone",
|
||||||
|
custom_scalars_module = "crate::infra::graphql"
|
||||||
|
)]
|
||||||
|
struct AddUserToGroup;
|
||||||
|
|
||||||
|
pub fn insert_group_memberships_into_lldap(
|
||||||
|
ldap_users: &[User],
|
||||||
|
ldap_groups: &[LdapGroup],
|
||||||
|
existing_users: &[String],
|
||||||
|
existing_groups: &[LldapGroup],
|
||||||
|
graphql_client: &GraphQLClient,
|
||||||
|
) -> Result<()> {
|
||||||
|
let existing_users = HashSet::<&str>::from_iter(existing_users.iter().map(String::as_str));
|
||||||
|
let existing_groups = HashMap::<&str, &LldapGroup>::from_iter(
|
||||||
|
existing_groups.iter().map(|g| (g.display_name.as_str(), g)),
|
||||||
|
);
|
||||||
|
let dn_resolver = HashMap::<&str, &str>::from_iter(
|
||||||
|
ldap_users
|
||||||
|
.iter()
|
||||||
|
.map(|u| (u.dn.as_str(), u.user_input.id.as_str())),
|
||||||
|
);
|
||||||
|
let mut skip_all = false;
|
||||||
|
let mut added_membership_count = 0;
|
||||||
|
for group in ldap_groups {
|
||||||
|
if let Some(lldap_group) = existing_groups.get(group.name.as_str()) {
|
||||||
|
let lldap_members =
|
||||||
|
HashSet::<&str>::from_iter(lldap_group.users.iter().map(|u| u.id.as_str()));
|
||||||
|
let mut skip_group = false;
|
||||||
|
for user in &group.members {
|
||||||
|
let user = if let Some(id) = dn_resolver.get(user.as_str()) {
|
||||||
|
id
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if lldap_members.contains(user) || !existing_users.contains(user) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
loop {
|
||||||
|
print!("Adding '{}' to '{}'... ", &user, &group.name);
|
||||||
|
if let Err(e) = graphql_client
|
||||||
|
.post::<AddUserToGroup>(add_user_to_group::Variables {
|
||||||
|
user: user.to_string(),
|
||||||
|
group: lldap_group.id,
|
||||||
|
})
|
||||||
|
.context(format!(
|
||||||
|
"while adding user '{}' to group '{}'",
|
||||||
|
&user, &group.name
|
||||||
|
))
|
||||||
|
{
|
||||||
|
println!("Error: {:#?}", e);
|
||||||
|
if skip_all || skip_group {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let question = requestty::Question::select("skip_membership")
|
||||||
|
.message(format!(
|
||||||
|
"Error while adding '{}' to group '{}",
|
||||||
|
&user, &group.name
|
||||||
|
))
|
||||||
|
.choices(vec!["Skip", "Retry", "Skip group", "Skip all"])
|
||||||
|
.default_separator()
|
||||||
|
.choice("Abort")
|
||||||
|
.build();
|
||||||
|
let answer = prompt_one(question)?;
|
||||||
|
let choice = answer.as_list_item().unwrap();
|
||||||
|
match choice.text.as_str() {
|
||||||
|
"Skip" => break,
|
||||||
|
"Retry" => continue,
|
||||||
|
"Skip group" => {
|
||||||
|
skip_group = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
"Skip all" => {
|
||||||
|
skip_all = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
"Abort" => return Err(e),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("Done!");
|
||||||
|
added_membership_count += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("{} memberships successfully added", added_membership_count);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_lldap_server(client: &Client) -> Result<String> {
|
||||||
|
let http_protocols = &[("http://", 17170), ("https://", 17170)];
|
||||||
|
let question = Question::input("lldap_url")
|
||||||
|
.message("LLDAP_URL (http://...)")
|
||||||
|
.auto_complete(|answer, _| {
|
||||||
|
let mut answers = SmallVec::<[String; 1]>::new();
|
||||||
|
if "http://".starts_with(&answer) {
|
||||||
|
answers.push("http://".to_owned());
|
||||||
|
}
|
||||||
|
if "https://".starts_with(&answer) {
|
||||||
|
answers.push("https://".to_owned());
|
||||||
|
}
|
||||||
|
answers.push(answer);
|
||||||
|
answers
|
||||||
|
})
|
||||||
|
.validate(|url, _| {
|
||||||
|
if let Some(url) = check_host_exists(url, http_protocols)? {
|
||||||
|
client
|
||||||
|
.get(format!("{}/api/graphql", url))
|
||||||
|
.send()
|
||||||
|
.map_err(|e| format!("Host did not answer: {}", e))
|
||||||
|
.and_then(|response| {
|
||||||
|
if response.status() == reqwest::StatusCode::UNAUTHORIZED {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err("Host doesn't seem to be an LLDAP server".to_owned())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(
|
||||||
|
"Could not resolve host (make sure it starts with 'http://' or 'https://')"
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
let answer = prompt_one(question)?;
|
||||||
|
Ok(
|
||||||
|
check_host_exists(answer.as_string().unwrap(), http_protocols)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
}
|
205
migration-tool/src/main.rs
Normal file
205
migration-tool/src/main.rs
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use requestty::{prompt_one, Question};
|
||||||
|
|
||||||
|
mod ldap;
|
||||||
|
mod lldap;
|
||||||
|
|
||||||
|
use ldap::LdapGroup;
|
||||||
|
use lldap::{LldapGroup, User};
|
||||||
|
|
||||||
|
fn ask_generic_confirmation(name: &str, message: &str) -> Result<bool> {
|
||||||
|
let confirm = Question::confirm(name)
|
||||||
|
.message(message)
|
||||||
|
.default(true)
|
||||||
|
.build();
|
||||||
|
let answer = prompt_one(confirm)?;
|
||||||
|
Ok(answer.as_bool().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_users_to_add(users: &[User], existing_users: &[String]) -> Result<Option<Vec<User>>> {
|
||||||
|
let existing_users = HashSet::<&String>::from_iter(existing_users);
|
||||||
|
let num_found_users = users.len();
|
||||||
|
let input_users: Vec<_> = users
|
||||||
|
.iter()
|
||||||
|
.filter(|u| !existing_users.contains(&u.user_input.id))
|
||||||
|
.map(User::clone)
|
||||||
|
.collect();
|
||||||
|
println!(
|
||||||
|
"Found {} users, of which {} new users: [\n {}\n]",
|
||||||
|
num_found_users,
|
||||||
|
input_users.len(),
|
||||||
|
input_users
|
||||||
|
.iter()
|
||||||
|
.map(|u| format!(
|
||||||
|
"\"{}\" ({})",
|
||||||
|
&u.user_input.id,
|
||||||
|
if u.password.is_some() {
|
||||||
|
"with password"
|
||||||
|
} else {
|
||||||
|
"no password"
|
||||||
|
}
|
||||||
|
))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(",\n ")
|
||||||
|
);
|
||||||
|
if !input_users.is_empty()
|
||||||
|
&& ask_generic_confirmation(
|
||||||
|
"proceed_users",
|
||||||
|
"Do you want to proceed to add those users to LLDAP?",
|
||||||
|
)?
|
||||||
|
{
|
||||||
|
Ok(Some(input_users))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_insert_groups(
|
||||||
|
input_groups: &[LdapGroup],
|
||||||
|
existing_groups: &[LldapGroup],
|
||||||
|
) -> Result<bool> {
|
||||||
|
let existing_group_names =
|
||||||
|
HashSet::<&str>::from_iter(existing_groups.iter().map(|g| g.display_name.as_str()));
|
||||||
|
let new_groups = input_groups
|
||||||
|
.iter()
|
||||||
|
.filter(|g| !existing_group_names.contains(g.name.as_str()));
|
||||||
|
let num_new_groups = new_groups.clone().count();
|
||||||
|
println!(
|
||||||
|
"Found {} groups, of which {} new groups: [\n {}\n]",
|
||||||
|
input_groups.len(),
|
||||||
|
num_new_groups,
|
||||||
|
new_groups
|
||||||
|
.map(|g| g.name.as_str())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(",\n ")
|
||||||
|
);
|
||||||
|
Ok(num_new_groups != 0
|
||||||
|
&& ask_generic_confirmation(
|
||||||
|
"proceed_groups",
|
||||||
|
"Do you want to proceed to add those groups to LLDAP?",
|
||||||
|
)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GroupList {
|
||||||
|
ldap_groups: Vec<LdapGroup>,
|
||||||
|
lldap_groups: Vec<LldapGroup>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn migrate_groups(
|
||||||
|
graphql_client: &lldap::GraphQLClient,
|
||||||
|
ldap_connection: &mut ldap::LdapClient,
|
||||||
|
) -> Result<Option<GroupList>> {
|
||||||
|
Ok(
|
||||||
|
if ask_generic_confirmation("should_import_groups", "Do you want to import groups?")? {
|
||||||
|
let mut existing_groups = lldap::get_lldap_groups(graphql_client)?;
|
||||||
|
let ldap_groups = ldap::get_groups(ldap_connection)?;
|
||||||
|
if should_insert_groups(&ldap_groups, &existing_groups)? {
|
||||||
|
lldap::insert_groups_into_lldap(
|
||||||
|
&ldap_groups,
|
||||||
|
&mut existing_groups,
|
||||||
|
graphql_client,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Some(GroupList {
|
||||||
|
ldap_groups,
|
||||||
|
lldap_groups: existing_groups,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UserList {
|
||||||
|
lldap_users: Vec<String>,
|
||||||
|
ldap_users: Vec<User>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn migrate_users(
|
||||||
|
graphql_client: &lldap::GraphQLClient,
|
||||||
|
ldap_connection: &mut ldap::LdapClient,
|
||||||
|
) -> Result<Option<UserList>> {
|
||||||
|
Ok(
|
||||||
|
if ask_generic_confirmation("should_import_users", "Do you want to import users?")? {
|
||||||
|
let mut existing_users = lldap::get_lldap_users(graphql_client)?;
|
||||||
|
let users = ldap::get_users(ldap_connection)?;
|
||||||
|
if let Some(users_to_add) = get_users_to_add(&users, &existing_users)? {
|
||||||
|
lldap::insert_users_into_lldap(users_to_add, &mut existing_users, graphql_client)?;
|
||||||
|
}
|
||||||
|
Some(UserList {
|
||||||
|
lldap_users: existing_users,
|
||||||
|
ldap_users: users,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn migrate_memberships(
|
||||||
|
user_list: Option<UserList>,
|
||||||
|
group_list: Option<GroupList>,
|
||||||
|
graphql_client: lldap::GraphQLClient,
|
||||||
|
ldap_connection: &mut ldap::LdapClient,
|
||||||
|
) -> Result<()> {
|
||||||
|
let (ldap_users, existing_users) = user_list
|
||||||
|
.map(
|
||||||
|
|UserList {
|
||||||
|
ldap_users,
|
||||||
|
lldap_users,
|
||||||
|
}| (Some(ldap_users), Some(lldap_users)),
|
||||||
|
)
|
||||||
|
.unwrap_or_default();
|
||||||
|
let (ldap_groups, existing_groups) = group_list
|
||||||
|
.map(
|
||||||
|
|GroupList {
|
||||||
|
ldap_groups,
|
||||||
|
lldap_groups,
|
||||||
|
}| (Some(ldap_groups), Some(lldap_groups)),
|
||||||
|
)
|
||||||
|
.unwrap_or_default();
|
||||||
|
let ldap_users = ldap_users
|
||||||
|
.ok_or_else(|| anyhow!("Missing LDAP users"))
|
||||||
|
.or_else(|_| ldap::get_users(ldap_connection))?;
|
||||||
|
let ldap_groups = ldap_groups
|
||||||
|
.ok_or_else(|| anyhow!("Missing LDAP groups"))
|
||||||
|
.or_else(|_| ldap::get_groups(ldap_connection))?;
|
||||||
|
let existing_groups = existing_groups
|
||||||
|
.ok_or_else(|| anyhow!("Missing LLDAP groups"))
|
||||||
|
.or_else(|_| lldap::get_lldap_groups(&graphql_client))?;
|
||||||
|
let existing_users = existing_users
|
||||||
|
.ok_or_else(|| anyhow!("Missing LLDAP users"))
|
||||||
|
.or_else(|_| lldap::get_lldap_users(&graphql_client))?;
|
||||||
|
lldap::insert_group_memberships_into_lldap(
|
||||||
|
&ldap_users,
|
||||||
|
&ldap_groups,
|
||||||
|
&existing_users,
|
||||||
|
&existing_groups,
|
||||||
|
&graphql_client,
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
println!(
|
||||||
|
"The migration tool requires access to both the original LDAP \
|
||||||
|
server and the HTTP API of the target LLDAP server."
|
||||||
|
);
|
||||||
|
if !ask_generic_confirmation("setup_ready", "Are you ready to start?")? {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let mut ldap_connection = ldap::get_ldap_connection()?;
|
||||||
|
let graphql_client = lldap::get_lldap_client()?;
|
||||||
|
let user_list = migrate_users(&graphql_client, &mut ldap_connection)?;
|
||||||
|
let group_list = migrate_groups(&graphql_client, &mut ldap_connection)?;
|
||||||
|
if ask_generic_confirmation(
|
||||||
|
"should_import_memberships",
|
||||||
|
"Do you want to import group memberships?",
|
||||||
|
)? {
|
||||||
|
migrate_memberships(user_list, group_list, graphql_client, &mut ldap_connection)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user