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
						f12e200c34
					
				
							
								
								
									
										253
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										253
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							@ -798,6 +798,31 @@ dependencies = [
 | 
			
		||||
 "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]]
 | 
			
		||||
name = "crypto-mac"
 | 
			
		||||
version = "0.10.1"
 | 
			
		||||
@ -1492,6 +1517,17 @@ dependencies = [
 | 
			
		||||
 "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]]
 | 
			
		||||
name = "http-range"
 | 
			
		||||
version = "0.1.4"
 | 
			
		||||
@ -1510,6 +1546,43 @@ version = "1.0.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
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]]
 | 
			
		||||
name = "ident_case"
 | 
			
		||||
version = "1.0.1"
 | 
			
		||||
@ -1559,6 +1632,12 @@ dependencies = [
 | 
			
		||||
 "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]]
 | 
			
		||||
name = "itertools"
 | 
			
		||||
version = "0.10.1"
 | 
			
		||||
@ -1686,6 +1765,31 @@ dependencies = [
 | 
			
		||||
 "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]]
 | 
			
		||||
name = "ldap3_server"
 | 
			
		||||
version = "0.1.9"
 | 
			
		||||
@ -1767,7 +1871,7 @@ checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "lldap"
 | 
			
		||||
version = "0.2.0"
 | 
			
		||||
version = "0.3.0-alpha.1"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "actix",
 | 
			
		||||
 "actix-files",
 | 
			
		||||
@ -1823,7 +1927,7 @@ dependencies = [
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "lldap_app"
 | 
			
		||||
version = "0.2.0"
 | 
			
		||||
version = "0.3.0-alpha.1"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "anyhow",
 | 
			
		||||
 "chrono",
 | 
			
		||||
@ -1847,7 +1951,7 @@ dependencies = [
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "lldap_auth"
 | 
			
		||||
version = "0.2.0"
 | 
			
		||||
version = "0.3.0-alpha.1"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "chrono",
 | 
			
		||||
 "curve25519-dalek",
 | 
			
		||||
@ -1933,6 +2037,22 @@ version = "2.4.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
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]]
 | 
			
		||||
name = "mime"
 | 
			
		||||
version = "0.3.16"
 | 
			
		||||
@ -2639,6 +2759,64 @@ dependencies = [
 | 
			
		||||
 "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]]
 | 
			
		||||
name = "rsa"
 | 
			
		||||
version = "0.3.0"
 | 
			
		||||
@ -2901,6 +3079,27 @@ dependencies = [
 | 
			
		||||
 "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]]
 | 
			
		||||
name = "signal-hook-registry"
 | 
			
		||||
version = "1.4.0"
 | 
			
		||||
@ -2942,6 +3141,12 @@ dependencies = [
 | 
			
		||||
 "static_assertions",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "smawk"
 | 
			
		||||
version = "0.3.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "socket2"
 | 
			
		||||
version = "0.4.1"
 | 
			
		||||
@ -3209,6 +3414,8 @@ version = "0.14.2"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "smawk",
 | 
			
		||||
 "unicode-linebreak",
 | 
			
		||||
 "unicode-width",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
@ -3379,6 +3586,12 @@ dependencies = [
 | 
			
		||||
 "serde",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "tower-service"
 | 
			
		||||
version = "0.3.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "tracing"
 | 
			
		||||
version = "0.1.26"
 | 
			
		||||
@ -3465,6 +3678,12 @@ version = "0.1.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "try-lock"
 | 
			
		||||
version = "0.2.3"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "typenum"
 | 
			
		||||
version = "1.14.0"
 | 
			
		||||
@ -3501,6 +3720,15 @@ version = "0.3.6"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
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]]
 | 
			
		||||
name = "unicode-normalization"
 | 
			
		||||
version = "0.1.19"
 | 
			
		||||
@ -3630,6 +3858,16 @@ version = "1.0.2"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
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]]
 | 
			
		||||
name = "wasi"
 | 
			
		||||
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"
 | 
			
		||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "winreg"
 | 
			
		||||
version = "0.7.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "winapi",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "wyz"
 | 
			
		||||
version = "0.2.0"
 | 
			
		||||
 | 
			
		||||
@ -2,9 +2,12 @@
 | 
			
		||||
members = [
 | 
			
		||||
  "server",
 | 
			
		||||
  "auth",
 | 
			
		||||
  "app"
 | 
			
		||||
  "app",
 | 
			
		||||
  "migration-tool"
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
default-members = ["server"]
 | 
			
		||||
 | 
			
		||||
# TODO: remove when there's a new release.
 | 
			
		||||
[patch.crates-io.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