This commit is contained in:
keerthi 2025-04-24 15:05:55 +05:30
commit ce1cf544a2
78 changed files with 1633 additions and 947 deletions

833
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -118,9 +118,11 @@ codegen-units = 256
# webauthn-rs-proto = { path = "../webauthn-rs/webauthn-rs-proto" } # webauthn-rs-proto = { path = "../webauthn-rs/webauthn-rs-proto" }
# sshkey-attest = { path = "../webauthn-rs/sshkey-attest" } # sshkey-attest = { path = "../webauthn-rs/sshkey-attest" }
# kanidm-hsm-crypto = { path = "../hsm-crypto" } # For BSD nss support
libnss = { git = "https://github.com/Firstyear/libnss-rs.git", branch = "20250207-freebsd" } libnss = { git = "https://github.com/Firstyear/libnss-rs.git", branch = "20250207-freebsd" }
# Allow ssh keys to have comments with spaces.
sshkeys = { git = "https://github.com/Firstyear/rust-sshkeys.git", rev = "3a081cbf7480628223bcb96fc8aaa8c19109d007" }
[workspace.dependencies] [workspace.dependencies]
kanidmd_core = { path = "./server/core", version = "=1.6.0-dev" } kanidmd_core = { path = "./server/core", version = "=1.6.0-dev" }
@ -138,7 +140,7 @@ kanidm_utils_users = { path = "./libs/users", version = "=1.6.0-dev" }
scim_proto = { path = "./libs/scim_proto", version = "=1.6.0-dev" } scim_proto = { path = "./libs/scim_proto", version = "=1.6.0-dev" }
sketching = { path = "./libs/sketching", version = "=1.6.0-dev" } sketching = { path = "./libs/sketching", version = "=1.6.0-dev" }
anyhow = { version = "1.0.95" } anyhow = { version = "1.0.98" }
argon2 = { version = "0.5.3", features = ["alloc"] } argon2 = { version = "0.5.3", features = ["alloc"] }
askama = { version = "0.12.1", features = ["serde", "with-axum"] } askama = { version = "0.12.1", features = ["serde", "with-axum"] }
askama_axum = { version = "0.4.0" } askama_axum = { version = "0.4.0" }
@ -159,7 +161,7 @@ base64 = "^0.22.1"
base64urlsafedata = "0.5.1" base64urlsafedata = "0.5.1"
bitflags = "^2.8.0" bitflags = "^2.8.0"
bytes = "^1.9.0" bytes = "^1.9.0"
clap = { version = "^4.5.34", features = ["derive", "env"] } clap = { version = "4.5.37", features = ["derive", "env"] }
clap_complete = "^4.5.42" clap_complete = "^4.5.42"
# Forced by saffron/cron # Forced by saffron/cron
chrono = "^0.4.39" chrono = "^0.4.39"
@ -176,16 +178,18 @@ filetime = "^0.2.24"
fs4 = "^0.13.0" fs4 = "^0.13.0"
futures = "^0.3.31" futures = "^0.3.31"
futures-util = { version = "^0.3.30", features = ["sink"] } futures-util = { version = "^0.3.30", features = ["sink"] }
gix = { version = "0.64.0", default-features = false } gix = { version = "0.71.0", default-features = false }
hashbrown = { version = "0.14.3", features = ["serde", "inline-more", "ahash"] } haproxy-protocol = { version = "0.0.1" }
hashbrown = { version = "0.15.2", features = ["serde", "inline-more"] }
hex = "^0.4.3" hex = "^0.4.3"
http = "1.2.0" http = "1.2.0"
http-body-util = "0.1"
hyper = { version = "1.5.1", features = [ hyper = { version = "1.5.1", features = [
"full", "full",
] } # hyper full includes client/server/http2 ] } # hyper full includes client/server/http2
hyper-util = { version = "0.1.10", features = ["server", "tokio"] } hyper-util = { version = "0.1.10", features = ["server", "tokio"] }
idlset = "^0.2.5" idlset = "^0.2.5"
image = { version = "0.24.9", default-features = false, features = [ image = { version = "0.25.6", default-features = false, features = [
"gif", "gif",
"jpeg", "jpeg",
"webp", "webp",
@ -198,16 +202,16 @@ lazy_static = "^1.5.0"
ldap3_client = "^0.5.2" ldap3_client = "^0.5.2"
ldap3_proto = { version = "^0.5.2", features = ["serde"] } ldap3_proto = { version = "^0.5.2", features = ["serde"] }
libc = "^0.2.168" libc = "0.2.172"
libnss = "^0.8.0" libnss = "^0.8.0"
libsqlite3-sys = "^0.25.2" libsqlite3-sys = "^0.25.2"
lodepng = "3.11.0" lodepng = "3.11.0"
lru = "^0.13.0" lru = "0.14.0"
mathru = "^0.13.0" mathru = "0.15.5"
md-5 = "0.10.6" md-5 = "0.10.6"
mimalloc = "0.1.43" mimalloc = "0.1.46"
notify-debouncer-full = { version = "0.5" } notify-debouncer-full = { version = "0.5" }
num_enum = "^0.5.11" num_enum = "0.7.3"
oauth2_ext = { version = "^4.4.2", package = "oauth2", default-features = false } oauth2_ext = { version = "^4.4.2", package = "oauth2", default-features = false }
openssl-sys = "^0.9" openssl-sys = "^0.9"
openssl = "^0.10.72" openssl = "^0.10.72"
@ -229,11 +233,11 @@ tracing-core = "0.1.33"
peg = "0.8" peg = "0.8"
pkg-config = "^0.3.31" pkg-config = "^0.3.31"
prctl = "1.0.0" prctl = "1.0.0"
proc-macro2 = "1.0.93" proc-macro2 = "1.0.95"
qrcode = "^0.12.0" qrcode = "0.14.1"
quote = "1" quote = "1"
rand = "^0.8.5" rand = "0.9.1"
rand_chacha = "0.3.1" rand_chacha = "0.9.0"
regex = "1.11.0" regex = "1.11.0"
reqwest = { version = "0.12.12", default-features = false, features = [ reqwest = { version = "0.12.12", default-features = false, features = [
"cookies", "cookies",
@ -243,13 +247,13 @@ reqwest = { version = "0.12.12", default-features = false, features = [
"rustls-tls-native-roots", "rustls-tls-native-roots",
"rustls-tls-native-roots-no-provider", "rustls-tls-native-roots-no-provider",
] } ] }
rusqlite = { version = "^0.28.0", features = ["array", "bundled"] } rusqlite = { version = "0.35.0", features = ["array", "bundled"] }
rustls = { version = "0.23.21", default-features = false, features = [ rustls = { version = "0.23.26", default-features = false, features = [
"aws_lc_rs", "aws_lc_rs",
] } ] }
sd-notify = "^0.4.5" sd-notify = "^0.4.5"
selinux = "^0.4.6" selinux = "^0.5.1"
serde = "^1.0.217" serde = "^1.0.217"
serde_cbor = { version = "0.12.0-dev", package = "serde_cbor_2" } serde_cbor = { version = "0.12.0-dev", package = "serde_cbor_2" }
serde_json = "^1.0.137" serde_json = "^1.0.137"
@ -257,13 +261,13 @@ serde_urlencoded = "^0.7.1"
serde_with = "3.12.0" serde_with = "3.12.0"
sha-crypt = "0.5.0" sha-crypt = "0.5.0"
sha2 = "0.10.8" sha2 = "0.10.8"
shellexpand = "^2.1.2" shellexpand = "3.1.1"
smartstring = "^1.0.1" smartstring = "^1.0.1"
smolset = "^1.3.1" smolset = "^1.3.1"
sshkey-attest = "^0.5.0" sshkey-attest = "^0.5.0"
sshkeys = "0.3.3" sshkeys = "0.3.3"
svg = "0.13.1" svg = "0.18.0"
syn = { version = "2.0.96", features = ["full"] } syn = { version = "2.0.100", features = ["full"] }
tempfile = "3.15.0" tempfile = "3.15.0"
testkit-macros = { path = "./server/testkit-macros" } testkit-macros = { path = "./server/testkit-macros" }
time = { version = "^0.3.36", features = ["formatting", "local-offset"] } time = { version = "^0.3.36", features = ["formatting", "local-offset"] }
@ -272,7 +276,7 @@ tokio = "^1.44.2"
tokio-openssl = "^0.6.5" tokio-openssl = "^0.6.5"
tokio-util = "^0.7.13" tokio-util = "^0.7.13"
toml = "^0.5.11" toml = "^0.8.20"
tracing = { version = "^0.1.41", features = [ tracing = { version = "^0.1.41", features = [
"max_level_trace", "max_level_trace",
"release_max_level_debug", "release_max_level_debug",
@ -299,6 +303,6 @@ walkdir = "2"
x509-cert = "0.2.5" x509-cert = "0.2.5"
zxcvbn = "^2.2.2" zxcvbn = "3.1.0"
nonempty = "0.8.1" nonempty = "0.11.0"

View file

@ -13,16 +13,6 @@ bindaddress = "[::]:443"
# Defaults to "" (disabled) # Defaults to "" (disabled)
# ldapbindaddress = "[::]:636" # ldapbindaddress = "[::]:636"
# #
# HTTPS requests can be reverse proxied by a loadbalancer.
# To preserve the original IP of the caller, these systems
# will often add a header such as "Forwarded" or
# "X-Forwarded-For". If set to true, then this header is
# respected as the "authoritative" source of the IP of the
# connected client. If you are not using a load balancer
# then you should leave this value as default.
# Defaults to false
# trust_x_forward_for = false
#
# The path to the kanidm database. # The path to the kanidm database.
db_path = "/var/lib/private/kanidm/kanidm.db" db_path = "/var/lib/private/kanidm/kanidm.db"
# #
@ -86,6 +76,32 @@ domain = "idm.example.com"
# origin = "https://idm.example.com" # origin = "https://idm.example.com"
origin = "https://idm.example.com:8443" origin = "https://idm.example.com:8443"
# #
# HTTPS requests can be reverse proxied by a loadbalancer.
# To preserve the original IP of the caller, these systems
# will often add a header such as "Forwarded" or
# "X-Forwarded-For". Some other proxies can use the PROXY
# protocol v2 header.
# This setting allows configuration of the range of trusted
# IPs which can supply this header information, and which
# format the information is provided in.
# Defaults to "none" (no trusted sources)
# Only one option can be used at a time.
# [http_client_address_info]
# proxy-v2 = ["127.0.0.1"]
# # OR
# x-forward-for = ["127.0.0.1"]
# LDAPS requests can be reverse proxied by a loadbalancer.
# To preserve the original IP of the caller, these systems
# can add a header such as the PROXY protocol v2 header.
# This setting allows configuration of the range of trusted
# IPs which can supply this header information, and which
# format the information is provided in.
# Defaults to "none" (no trusted sources)
# [ldap_client_address_info]
# proxy-v2 = ["127.0.0.1"]
[online_backup] [online_backup]
# The path to the output folder for online backups # The path to the output folder for online backups
path = "/var/lib/private/kanidm/backups/" path = "/var/lib/private/kanidm/backups/"

View file

@ -13,16 +13,6 @@ bindaddress = "[::]:8443"
# Defaults to "" (disabled) # Defaults to "" (disabled)
# ldapbindaddress = "[::]:3636" # ldapbindaddress = "[::]:3636"
# #
# HTTPS requests can be reverse proxied by a loadbalancer.
# To preserve the original IP of the caller, these systems
# will often add a header such as "Forwarded" or
# "X-Forwarded-For". If set to true, then this header is
# respected as the "authoritative" source of the IP of the
# connected client. If you are not using a load balancer
# then you should leave this value as default.
# Defaults to false
# trust_x_forward_for = false
#
# The path to the kanidm database. # The path to the kanidm database.
db_path = "/data/kanidm.db" db_path = "/data/kanidm.db"
# #
@ -85,7 +75,32 @@ domain = "idm.example.com"
# not consistent, the server WILL refuse to start! # not consistent, the server WILL refuse to start!
# origin = "https://idm.example.com" # origin = "https://idm.example.com"
origin = "https://idm.example.com:8443" origin = "https://idm.example.com:8443"
#
# HTTPS requests can be reverse proxied by a loadbalancer.
# To preserve the original IP of the caller, these systems
# will often add a header such as "Forwarded" or
# "X-Forwarded-For". Some other proxies can use the PROXY
# protocol v2 header.
# This setting allows configuration of the range of trusted
# IPs which can supply this header information, and which
# format the information is provided in.
# Defaults to "none" (no trusted sources)
# Only one option can be used at a time.
# [http_client_address_info]
# proxy-v2 = ["127.0.0.1"]
# # OR
# x-forward-for = ["127.0.0.1"]
# LDAPS requests can be reverse proxied by a loadbalancer.
# To preserve the original IP of the caller, these systems
# can add a header such as the PROXY protocol v2 header.
# This setting allows configuration of the range of trusted
# IPs which can supply this header information, and which
# format the information is provided in.
# Defaults to "none" (no trusted sources)
# [ldap_client_address_info]
# proxy-v2 = ["127.0.0.1"]
[online_backup] [online_backup]
# The path to the output folder for online backups # The path to the output folder for online backups
path = "/data/kanidm/backups/" path = "/data/kanidm/backups/"

View file

@ -834,9 +834,9 @@ impl TryFrom<&str> for Password {
impl Password { impl Password {
fn bench_pbkdf2(pbkdf2_cost: usize) -> Option<Duration> { fn bench_pbkdf2(pbkdf2_cost: usize) -> Option<Duration> {
let mut rng = rand::thread_rng(); let mut rng = rand::rng();
let salt: Vec<u8> = (0..PBKDF2_SALT_LEN).map(|_| rng.gen()).collect(); let salt: Vec<u8> = (0..PBKDF2_SALT_LEN).map(|_| rng.random()).collect();
let input: Vec<u8> = (0..PBKDF2_SALT_LEN).map(|_| rng.gen()).collect(); let input: Vec<u8> = (0..PBKDF2_SALT_LEN).map(|_| rng.random()).collect();
// This is 512 bits of output // This is 512 bits of output
let mut key: Vec<u8> = (0..PBKDF2_KEY_LEN).map(|_| 0).collect(); let mut key: Vec<u8> = (0..PBKDF2_KEY_LEN).map(|_| 0).collect();
@ -855,9 +855,9 @@ impl Password {
} }
fn bench_argon2id(params: Params) -> Option<Duration> { fn bench_argon2id(params: Params) -> Option<Duration> {
let mut rng = rand::thread_rng(); let mut rng = rand::rng();
let salt: Vec<u8> = (0..ARGON2_SALT_LEN).map(|_| rng.gen()).collect(); let salt: Vec<u8> = (0..ARGON2_SALT_LEN).map(|_| rng.random()).collect();
let input: Vec<u8> = (0..ARGON2_SALT_LEN).map(|_| rng.gen()).collect(); let input: Vec<u8> = (0..ARGON2_SALT_LEN).map(|_| rng.random()).collect();
let mut key: Vec<u8> = (0..ARGON2_KEY_LEN).map(|_| 0).collect(); let mut key: Vec<u8> = (0..ARGON2_KEY_LEN).map(|_| 0).collect();
let argon = Argon2::new(Algorithm::Argon2id, Version::V0x13, params); let argon = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
@ -873,8 +873,8 @@ impl Password {
pub fn new_pbkdf2(policy: &CryptoPolicy, cleartext: &str) -> Result<Self, CryptoError> { pub fn new_pbkdf2(policy: &CryptoPolicy, cleartext: &str) -> Result<Self, CryptoError> {
let pbkdf2_cost = policy.pbkdf2_cost; let pbkdf2_cost = policy.pbkdf2_cost;
let mut rng = rand::thread_rng(); let mut rng = rand::rng();
let salt: Vec<u8> = (0..PBKDF2_SALT_LEN).map(|_| rng.gen()).collect(); let salt: Vec<u8> = (0..PBKDF2_SALT_LEN).map(|_| rng.random()).collect();
let mut key: Vec<u8> = (0..PBKDF2_KEY_LEN).map(|_| 0).collect(); let mut key: Vec<u8> = (0..PBKDF2_KEY_LEN).map(|_| 0).collect();
pbkdf2_hmac( pbkdf2_hmac(
@ -897,8 +897,8 @@ impl Password {
let argon = Argon2::new(Algorithm::Argon2id, version, policy.argon2id_params.clone()); let argon = Argon2::new(Algorithm::Argon2id, version, policy.argon2id_params.clone());
let mut rng = rand::thread_rng(); let mut rng = rand::rng();
let salt: Vec<u8> = (0..ARGON2_SALT_LEN).map(|_| rng.gen()).collect(); let salt: Vec<u8> = (0..ARGON2_SALT_LEN).map(|_| rng.random()).collect();
let mut key: Vec<u8> = (0..ARGON2_KEY_LEN).map(|_| 0).collect(); let mut key: Vec<u8> = (0..ARGON2_KEY_LEN).map(|_| 0).collect();
argon argon
@ -925,8 +925,8 @@ impl Password {
let argon = Argon2::new(Algorithm::Argon2id, version, policy.argon2id_params.clone()); let argon = Argon2::new(Algorithm::Argon2id, version, policy.argon2id_params.clone());
let mut rng = rand::thread_rng(); let mut rng = rand::rng();
let salt: Vec<u8> = (0..ARGON2_SALT_LEN).map(|_| rng.gen()).collect(); let salt: Vec<u8> = (0..ARGON2_SALT_LEN).map(|_| rng.random()).collect();
let mut check_key: Vec<u8> = (0..ARGON2_KEY_LEN).map(|_| 0).collect(); let mut check_key: Vec<u8> = (0..ARGON2_KEY_LEN).map(|_| 0).collect();
argon argon

View file

@ -77,7 +77,10 @@ pub fn apply_profile() {
.decode(contents) .decode(contents)
.unwrap_or_else(|_| panic!("Failed to parse profile - {} - {}", profile, contents)); .unwrap_or_else(|_| panic!("Failed to parse profile - {} - {}", profile, contents));
let profile_cfg: ProfileConfig = toml::from_slice(&data) let data_str = String::from_utf8(data)
.unwrap_or_else(|_| panic!("Failed to read profile data to UTF-8 string - {}", profile));
let profile_cfg: ProfileConfig = toml::from_str(&data_str)
.unwrap_or_else(|_| panic!("Failed to parse profile - {} - {}", profile, contents)); .unwrap_or_else(|_| panic!("Failed to parse profile - {} - {}", profile, contents));
// We have to setup for our pkg version to be passed into things correctly // We have to setup for our pkg version to be passed into things correctly

View file

@ -34,6 +34,8 @@ cron = { workspace = true }
filetime = { workspace = true } filetime = { workspace = true }
futures = { workspace = true } futures = { workspace = true }
futures-util = { workspace = true } futures-util = { workspace = true }
haproxy-protocol = { workspace = true, features = ["tokio"] }
hashbrown = { workspace = true }
hyper = { workspace = true } hyper = { workspace = true }
hyper-util = { workspace = true } hyper-util = { workspace = true }
kanidm_proto = { workspace = true } kanidm_proto = { workspace = true }

View file

@ -4,18 +4,18 @@
//! These components should be "per server". Any "per domain" config should be in the system //! These components should be "per server". Any "per domain" config should be in the system
//! or domain entries that are able to be replicated. //! or domain entries that are able to be replicated.
use std::fmt::{self, Display}; use hashbrown::HashSet;
use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use kanidm_proto::constants::DEFAULT_SERVER_ADDRESS; use kanidm_proto::constants::DEFAULT_SERVER_ADDRESS;
use kanidm_proto::internal::FsType; use kanidm_proto::internal::FsType;
use kanidm_proto::messages::ConsoleOutputMode; use kanidm_proto::messages::ConsoleOutputMode;
use serde::Deserialize; use serde::Deserialize;
use sketching::LogLevel; use sketching::LogLevel;
use std::fmt::{self, Display};
use std::fs::File;
use std::io::Read;
use std::net::IpAddr;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use url::Url; use url::Url;
use crate::repl::config::ReplicationConfiguration; use crate::repl::config::ReplicationConfiguration;
@ -100,6 +100,111 @@ pub struct TlsConfiguration {
pub client_ca: Option<PathBuf>, pub client_ca: Option<PathBuf>,
} }
#[derive(Deserialize, Debug, Clone, Default)]
pub enum LdapAddressInfo {
#[default]
None,
#[serde(rename = "proxy-v2")]
ProxyV2(HashSet<IpAddr>),
}
impl LdapAddressInfo {
pub fn trusted_proxy_v2(&self) -> Option<HashSet<IpAddr>> {
if let Self::ProxyV2(trusted) = self {
Some(trusted.clone())
} else {
None
}
}
}
impl Display for LdapAddressInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::None => f.write_str("none"),
Self::ProxyV2(trusted) => {
f.write_str("proxy-v2 [ ")?;
for ip in trusted {
write!(f, "{} ", ip)?;
}
f.write_str("]")
}
}
}
}
pub(crate) enum AddressSet {
NonContiguousIpSet(HashSet<IpAddr>),
All,
}
impl AddressSet {
pub(crate) fn contains(&self, ip_addr: &IpAddr) -> bool {
match self {
Self::All => true,
Self::NonContiguousIpSet(range) => range.contains(ip_addr),
}
}
}
#[derive(Deserialize, Debug, Clone, Default)]
pub enum HttpAddressInfo {
#[default]
None,
#[serde(rename = "x-forward-for")]
XForwardFor(HashSet<IpAddr>),
// IMPORTANT: This is undocumented, and only exists for backwards compat
// with config v1 which has a boolean toggle for this option.
#[serde(rename = "x-forward-for-all-source-trusted")]
XForwardForAllSourcesTrusted,
#[serde(rename = "proxy-v2")]
ProxyV2(HashSet<IpAddr>),
}
impl HttpAddressInfo {
pub(crate) fn trusted_x_forward_for(&self) -> Option<AddressSet> {
match self {
Self::XForwardForAllSourcesTrusted => Some(AddressSet::All),
Self::XForwardFor(trusted) => Some(AddressSet::NonContiguousIpSet(trusted.clone())),
_ => None,
}
}
pub(crate) fn trusted_proxy_v2(&self) -> Option<HashSet<IpAddr>> {
if let Self::ProxyV2(trusted) = self {
Some(trusted.clone())
} else {
None
}
}
}
impl Display for HttpAddressInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::None => f.write_str("none"),
Self::XForwardFor(trusted) => {
f.write_str("x-forward-for [ ")?;
for ip in trusted {
write!(f, "{} ", ip)?;
}
f.write_str("]")
}
Self::XForwardForAllSourcesTrusted => {
f.write_str("x-forward-for [ ALL SOURCES TRUSTED ]")
}
Self::ProxyV2(trusted) => {
f.write_str("proxy-v2 [ ")?;
for ip in trusted {
write!(f, "{} ", ip)?;
}
f.write_str("]")
}
}
}
}
/// This is the Server Configuration as read from `server.toml` or environment variables. /// This is the Server Configuration as read from `server.toml` or environment variables.
/// ///
/// Fields noted as "REQUIRED" are required for the server to start, even if they show as optional due to how file parsing works. /// Fields noted as "REQUIRED" are required for the server to start, even if they show as optional due to how file parsing works.
@ -217,7 +322,10 @@ pub struct ServerConfigV2 {
role: Option<ServerRole>, role: Option<ServerRole>,
log_level: Option<LogLevel>, log_level: Option<LogLevel>,
online_backup: Option<OnlineBackup>, online_backup: Option<OnlineBackup>,
trust_x_forward_for: Option<bool>,
http_client_address_info: Option<HttpAddressInfo>,
ldap_client_address_info: Option<LdapAddressInfo>,
adminbindpath: Option<String>, adminbindpath: Option<String>,
thread_count: Option<usize>, thread_count: Option<usize>,
maximum_request_size_bytes: Option<usize>, maximum_request_size_bytes: Option<usize>,
@ -490,7 +598,10 @@ pub struct Configuration {
pub db_fs_type: Option<FsType>, pub db_fs_type: Option<FsType>,
pub db_arc_size: Option<usize>, pub db_arc_size: Option<usize>,
pub maximum_request: usize, pub maximum_request: usize,
pub trust_x_forward_for: bool,
pub http_client_address_info: HttpAddressInfo,
pub ldap_client_address_info: LdapAddressInfo,
pub tls_config: Option<TlsConfiguration>, pub tls_config: Option<TlsConfiguration>,
pub integration_test_config: Option<Box<IntegrationTestConfig>>, pub integration_test_config: Option<Box<IntegrationTestConfig>>,
pub online_backup: Option<OnlineBackup>, pub online_backup: Option<OnlineBackup>,
@ -522,7 +633,8 @@ impl Configuration {
db_fs_type: None, db_fs_type: None,
db_arc_size: None, db_arc_size: None,
maximum_request: 256 * 1024, // 256k maximum_request: 256 * 1024, // 256k
trust_x_forward_for: None, http_client_address_info: HttpAddressInfo::default(),
ldap_client_address_info: LdapAddressInfo::default(),
tls_key: None, tls_key: None,
tls_chain: None, tls_chain: None,
tls_client_ca: None, tls_client_ca: None,
@ -547,7 +659,8 @@ impl Configuration {
db_fs_type: None, db_fs_type: None,
db_arc_size: None, db_arc_size: None,
maximum_request: 256 * 1024, // 256k maximum_request: 256 * 1024, // 256k
trust_x_forward_for: false, http_client_address_info: HttpAddressInfo::default(),
ldap_client_address_info: LdapAddressInfo::default(),
tls_config: None, tls_config: None,
integration_test_config: None, integration_test_config: None,
online_backup: None, online_backup: None,
@ -587,7 +700,17 @@ impl fmt::Display for Configuration {
None => write!(f, "arcsize: AUTO, "), None => write!(f, "arcsize: AUTO, "),
}?; }?;
write!(f, "max request size: {}b, ", self.maximum_request)?; write!(f, "max request size: {}b, ", self.maximum_request)?;
write!(f, "trust X-Forwarded-For: {}, ", self.trust_x_forward_for)?; write!(
f,
"http client address info: {}, ",
self.http_client_address_info
)?;
write!(
f,
"ldap client address info: {}, ",
self.ldap_client_address_info
)?;
write!(f, "with TLS: {}, ", self.tls_config.is_some())?; write!(f, "with TLS: {}, ", self.tls_config.is_some())?;
match &self.online_backup { match &self.online_backup {
Some(bck) => write!( Some(bck) => write!(
@ -642,7 +765,8 @@ pub struct ConfigurationBuilder {
db_fs_type: Option<FsType>, db_fs_type: Option<FsType>,
db_arc_size: Option<usize>, db_arc_size: Option<usize>,
maximum_request: usize, maximum_request: usize,
trust_x_forward_for: Option<bool>, http_client_address_info: HttpAddressInfo,
ldap_client_address_info: LdapAddressInfo,
tls_key: Option<PathBuf>, tls_key: Option<PathBuf>,
tls_chain: Option<PathBuf>, tls_chain: Option<PathBuf>,
tls_client_ca: Option<PathBuf>, tls_client_ca: Option<PathBuf>,
@ -691,8 +815,8 @@ impl ConfigurationBuilder {
self.db_arc_size = env_config.db_arc_size; self.db_arc_size = env_config.db_arc_size;
} }
if env_config.trust_x_forward_for.is_some() { if env_config.trust_x_forward_for == Some(true) {
self.trust_x_forward_for = env_config.trust_x_forward_for; self.http_client_address_info = HttpAddressInfo::XForwardForAllSourcesTrusted;
} }
if env_config.tls_key.is_some() { if env_config.tls_key.is_some() {
@ -813,8 +937,8 @@ impl ConfigurationBuilder {
self.db_arc_size = config.db_arc_size; self.db_arc_size = config.db_arc_size;
} }
if config.trust_x_forward_for.is_some() { if config.trust_x_forward_for == Some(true) {
self.trust_x_forward_for = config.trust_x_forward_for; self.http_client_address_info = HttpAddressInfo::XForwardForAllSourcesTrusted;
} }
if config.online_backup.is_some() { if config.online_backup.is_some() {
@ -893,8 +1017,12 @@ impl ConfigurationBuilder {
self.db_arc_size = config.db_arc_size; self.db_arc_size = config.db_arc_size;
} }
if config.trust_x_forward_for.is_some() { if let Some(http_client_address_info) = config.http_client_address_info {
self.trust_x_forward_for = config.trust_x_forward_for; self.http_client_address_info = http_client_address_info
}
if let Some(ldap_client_address_info) = config.ldap_client_address_info {
self.ldap_client_address_info = ldap_client_address_info
} }
if config.online_backup.is_some() { if config.online_backup.is_some() {
@ -930,7 +1058,8 @@ impl ConfigurationBuilder {
db_fs_type, db_fs_type,
db_arc_size, db_arc_size,
maximum_request, maximum_request,
trust_x_forward_for, http_client_address_info,
ldap_client_address_info,
tls_key, tls_key,
tls_chain, tls_chain,
tls_client_ca, tls_client_ca,
@ -986,7 +1115,6 @@ impl ConfigurationBuilder {
let adminbindpath = let adminbindpath =
adminbindpath.unwrap_or(env!("KANIDM_SERVER_ADMIN_BIND_PATH").to_string()); adminbindpath.unwrap_or(env!("KANIDM_SERVER_ADMIN_BIND_PATH").to_string());
let address = bindaddress.unwrap_or(DEFAULT_SERVER_ADDRESS.to_string()); let address = bindaddress.unwrap_or(DEFAULT_SERVER_ADDRESS.to_string());
let trust_x_forward_for = trust_x_forward_for.unwrap_or_default();
let output_mode = output_mode.unwrap_or_default(); let output_mode = output_mode.unwrap_or_default();
let role = role.unwrap_or(ServerRole::WriteReplica); let role = role.unwrap_or(ServerRole::WriteReplica);
let log_level = log_level.unwrap_or_default(); let log_level = log_level.unwrap_or_default();
@ -1000,7 +1128,8 @@ impl ConfigurationBuilder {
db_fs_type, db_fs_type,
db_arc_size, db_arc_size,
maximum_request, maximum_request,
trust_x_forward_for, http_client_address_info,
ldap_client_address_info,
tls_config, tls_config,
online_backup, online_backup,
domain, domain,

View file

@ -5,7 +5,6 @@ use axum::{
http::{ http::{
header::HeaderName, header::AUTHORIZATION as AUTHORISATION, request::Parts, StatusCode, header::HeaderName, header::AUTHORIZATION as AUTHORISATION, request::Parts, StatusCode,
}, },
serve::IncomingStream,
RequestPartsExt, RequestPartsExt,
}; };
@ -40,7 +39,8 @@ impl FromRequestParts<ServerState> for TrustedClientIp {
state: &ServerState, state: &ServerState,
) -> Result<Self, Self::Rejection> { ) -> Result<Self, Self::Rejection> {
let ConnectInfo(ClientConnInfo { let ConnectInfo(ClientConnInfo {
addr, connection_addr,
client_addr,
client_cert: _, client_cert: _,
}) = parts }) = parts
.extract::<ConnectInfo<ClientConnInfo>>() .extract::<ConnectInfo<ClientConnInfo>>()
@ -53,7 +53,13 @@ impl FromRequestParts<ServerState> for TrustedClientIp {
) )
})?; })?;
let ip_addr = if state.trust_x_forward_for { let trust_x_forward_for = state
.trust_x_forward_for_ips
.as_ref()
.map(|range| range.contains(&connection_addr.ip()))
.unwrap_or_default();
let ip_addr = if trust_x_forward_for {
if let Some(x_forward_for) = parts.headers.get(X_FORWARDED_FOR_HEADER) { if let Some(x_forward_for) = parts.headers.get(X_FORWARDED_FOR_HEADER) {
// X forward for may be comma separated. // X forward for may be comma separated.
let first = x_forward_for let first = x_forward_for
@ -75,10 +81,14 @@ impl FromRequestParts<ServerState> for TrustedClientIp {
) )
})? })?
} else { } else {
addr.ip() client_addr.ip()
} }
} else { } else {
addr.ip() // This can either be the client_addr == connection_addr if there are
// no ip address trust sources, or this is the value as reported by
// proxy protocol header. If the proxy protocol header is used, then
// trust_x_forward_for can never have been true so we catch here.
client_addr.ip()
}; };
Ok(TrustedClientIp(ip_addr)) Ok(TrustedClientIp(ip_addr))
@ -97,7 +107,11 @@ impl FromRequestParts<ServerState> for VerifiedClientInformation {
parts: &mut Parts, parts: &mut Parts,
state: &ServerState, state: &ServerState,
) -> Result<Self, Self::Rejection> { ) -> Result<Self, Self::Rejection> {
let ConnectInfo(ClientConnInfo { addr, client_cert }) = parts let ConnectInfo(ClientConnInfo {
connection_addr,
client_addr,
client_cert,
}) = parts
.extract::<ConnectInfo<ClientConnInfo>>() .extract::<ConnectInfo<ClientConnInfo>>()
.await .await
.map_err(|_| { .map_err(|_| {
@ -108,7 +122,13 @@ impl FromRequestParts<ServerState> for VerifiedClientInformation {
) )
})?; })?;
let ip_addr = if state.trust_x_forward_for { let trust_x_forward_for = state
.trust_x_forward_for_ips
.as_ref()
.map(|range| range.contains(&connection_addr.ip()))
.unwrap_or_default();
let ip_addr = if trust_x_forward_for {
if let Some(x_forward_for) = parts.headers.get(X_FORWARDED_FOR_HEADER) { if let Some(x_forward_for) = parts.headers.get(X_FORWARDED_FOR_HEADER) {
// X forward for may be comma separated. // X forward for may be comma separated.
let first = x_forward_for let first = x_forward_for
@ -130,10 +150,10 @@ impl FromRequestParts<ServerState> for VerifiedClientInformation {
) )
})? })?
} else { } else {
addr.ip() client_addr.ip()
} }
} else { } else {
addr.ip() client_addr.ip()
}; };
let (basic_authz, bearer_token) = if let Some(header) = parts.headers.get(AUTHORISATION) { let (basic_authz, bearer_token) = if let Some(header) = parts.headers.get(AUTHORISATION) {
@ -201,30 +221,30 @@ impl FromRequestParts<ServerState> for DomainInfo {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ClientConnInfo { pub struct ClientConnInfo {
pub addr: SocketAddr, /// This is the address that is *connected* to Kanidm right now
/// for this operation.
#[allow(dead_code)]
pub connection_addr: SocketAddr,
/// This is the client address as reported by a remote IP source
/// such as x-forward-for or the PROXY protocol header
pub client_addr: SocketAddr,
// Only set if the certificate is VALID // Only set if the certificate is VALID
pub client_cert: Option<ClientCertInfo>, pub client_cert: Option<ClientCertInfo>,
} }
// This is the normal way that our extractors get the ip info
impl Connected<ClientConnInfo> for ClientConnInfo { impl Connected<ClientConnInfo> for ClientConnInfo {
fn connect_info(target: ClientConnInfo) -> Self { fn connect_info(target: ClientConnInfo) -> Self {
target target
} }
} }
// This is only used for plaintext http - in other words, integration tests only.
impl Connected<SocketAddr> for ClientConnInfo { impl Connected<SocketAddr> for ClientConnInfo {
fn connect_info(addr: SocketAddr) -> Self { fn connect_info(connection_addr: SocketAddr) -> Self {
ClientConnInfo { ClientConnInfo {
addr, client_addr: connection_addr,
client_cert: None, connection_addr,
}
}
}
impl Connected<IncomingStream<'_>> for ClientConnInfo {
fn connect_info(target: IncomingStream<'_>) -> Self {
ClientConnInfo {
addr: target.remote_addr(),
client_cert: None, client_cert: None,
} }
} }

View file

@ -17,9 +17,8 @@ mod views;
use self::extractors::ClientConnInfo; use self::extractors::ClientConnInfo;
use self::javascript::*; use self::javascript::*;
use crate::actors::{QueryServerReadV1, QueryServerWriteV1}; use crate::actors::{QueryServerReadV1, QueryServerWriteV1};
use crate::config::{Configuration, ServerRole}; use crate::config::{AddressSet, Configuration, ServerRole};
use crate::CoreAction; use crate::CoreAction;
use axum::{ use axum::{
body::Body, body::Body,
extract::connect_info::IntoMakeServiceWithConnectInfo, extract::connect_info::IntoMakeServiceWithConnectInfo,
@ -29,22 +28,28 @@ use axum::{
routing::*, routing::*,
Router, Router,
}; };
use axum_extra::extract::cookie::CookieJar; use axum_extra::extract::cookie::CookieJar;
use compact_jwt::{error::JwtError, JwsCompact, JwsHs256Signer, JwsVerifier}; use compact_jwt::{error::JwtError, JwsCompact, JwsHs256Signer, JwsVerifier};
use futures::pin_mut; use futures::pin_mut;
use haproxy_protocol::{ProxyHdrV2, RemoteAddress};
use hashbrown::HashSet;
use hyper::body::Incoming; use hyper::body::Incoming;
use hyper_util::rt::{TokioExecutor, TokioIo}; use hyper_util::rt::{TokioExecutor, TokioIo};
use kanidm_lib_crypto::x509_cert::{der::Decode, x509_public_key_s256, Certificate};
use kanidm_proto::{constants::KSESSIONID, internal::COOKIE_AUTH_SESSION_ID}; use kanidm_proto::{constants::KSESSIONID, internal::COOKIE_AUTH_SESSION_ID};
use kanidmd_lib::{idm::ClientCertInfo, status::StatusActor}; use kanidmd_lib::{idm::ClientCertInfo, status::StatusActor};
use openssl::ssl::{Ssl, SslAcceptor}; use openssl::ssl::{Ssl, SslAcceptor};
use kanidm_lib_crypto::x509_cert::{der::Decode, x509_public_key_s256, Certificate};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use sketching::*; use sketching::*;
use std::fmt::Write; use std::fmt::Write;
use std::io::ErrorKind;
use std::net::IpAddr;
use std::path::PathBuf;
use std::pin::Pin;
use std::sync::Arc;
use std::{net::SocketAddr, str::FromStr};
use tokio::{ use tokio::{
io::{AsyncRead, AsyncWrite},
net::{TcpListener, TcpStream}, net::{TcpListener, TcpStream},
sync::broadcast, sync::broadcast,
sync::mpsc, sync::mpsc,
@ -56,11 +61,6 @@ use tower_http::{services::ServeDir, trace::TraceLayer};
use url::Url; use url::Url;
use uuid::Uuid; use uuid::Uuid;
use std::io::ErrorKind;
use std::path::PathBuf;
use std::pin::Pin;
use std::{net::SocketAddr, str::FromStr};
#[derive(Clone)] #[derive(Clone)]
pub struct ServerState { pub struct ServerState {
pub(crate) status_ref: &'static StatusActor, pub(crate) status_ref: &'static StatusActor,
@ -68,7 +68,7 @@ pub struct ServerState {
pub(crate) qe_r_ref: &'static QueryServerReadV1, pub(crate) qe_r_ref: &'static QueryServerReadV1,
// Store the token management parts. // Store the token management parts.
pub(crate) jws_signer: JwsHs256Signer, pub(crate) jws_signer: JwsHs256Signer,
pub(crate) trust_x_forward_for: bool, pub(crate) trust_x_forward_for_ips: Option<Arc<AddressSet>>,
pub(crate) csp_header: HeaderValue, pub(crate) csp_header: HeaderValue,
pub(crate) origin: Url, pub(crate) origin: Url,
pub(crate) domain: String, pub(crate) domain: String,
@ -211,7 +211,15 @@ pub async fn create_https_server(
error!(?err, "Unable to generate content security policy"); error!(?err, "Unable to generate content security policy");
})?; })?;
let trust_x_forward_for = config.trust_x_forward_for; let trust_x_forward_for_ips = config
.http_client_address_info
.trusted_x_forward_for()
.map(Arc::new);
let trusted_proxy_v2_ips = config
.http_client_address_info
.trusted_proxy_v2()
.map(Arc::new);
let origin = Url::parse(&config.origin) let origin = Url::parse(&config.origin)
// Should be impossible! // Should be impossible!
@ -224,7 +232,7 @@ pub async fn create_https_server(
qe_w_ref, qe_w_ref,
qe_r_ref, qe_r_ref,
jws_signer, jws_signer,
trust_x_forward_for, trust_x_forward_for_ips,
csp_header, csp_header,
origin, origin,
domain: config.domain.clone(), domain: config.domain.clone(),
@ -321,35 +329,41 @@ pub async fn create_https_server(
info!("Starting the web server..."); info!("Starting the web server...");
match maybe_tls_acceptor { let listener = match TcpListener::bind(addr).await {
Some(tls_acceptor) => { Ok(l) => l,
let listener = match TcpListener::bind(addr).await { Err(err) => {
Ok(l) => l, error!(?err, "Failed to bind tcp listener");
Err(err) => { return Err(());
error!(?err, "Failed to bind tcp listener");
return Err(());
}
};
Ok(task::spawn(server_loop(
tls_acceptor,
listener,
app,
rx,
server_message_tx,
tls_acceptor_reload_rx,
)))
} }
None => Ok(task::spawn(server_loop_plaintext(addr, app, rx))), };
match maybe_tls_acceptor {
Some(tls_acceptor) => Ok(task::spawn(server_tls_loop(
tls_acceptor,
listener,
app,
rx,
server_message_tx,
tls_acceptor_reload_rx,
trusted_proxy_v2_ips,
))),
None => Ok(task::spawn(server_plaintext_loop(
listener,
app,
rx,
trusted_proxy_v2_ips,
))),
} }
} }
async fn server_loop( async fn server_tls_loop(
mut tls_acceptor: SslAcceptor, mut tls_acceptor: SslAcceptor,
listener: TcpListener, listener: TcpListener,
app: IntoMakeServiceWithConnectInfo<Router, ClientConnInfo>, app: IntoMakeServiceWithConnectInfo<Router, ClientConnInfo>,
mut rx: broadcast::Receiver<CoreAction>, mut rx: broadcast::Receiver<CoreAction>,
server_message_tx: broadcast::Sender<CoreAction>, server_message_tx: broadcast::Sender<CoreAction>,
mut tls_acceptor_reload_rx: mpsc::Receiver<SslAcceptor>, mut tls_acceptor_reload_rx: mpsc::Receiver<SslAcceptor>,
trusted_proxy_v2_ips: Option<Arc<HashSet<IpAddr>>>,
) { ) {
pin_mut!(listener); pin_mut!(listener);
@ -365,7 +379,7 @@ async fn server_loop(
Ok((stream, addr)) => { Ok((stream, addr)) => {
let tls_acceptor = tls_acceptor.clone(); let tls_acceptor = tls_acceptor.clone();
let app = app.clone(); let app = app.clone();
task::spawn(handle_conn(tls_acceptor, stream, app, addr)); task::spawn(handle_tls_conn(tls_acceptor, stream, app, addr, trusted_proxy_v2_ips.clone()));
} }
Err(err) => { Err(err) => {
error!("Web server exited with {:?}", err); error!("Web server exited with {:?}", err);
@ -386,24 +400,33 @@ async fn server_loop(
info!("Stopped {}", super::TaskName::HttpsServer); info!("Stopped {}", super::TaskName::HttpsServer);
} }
async fn server_loop_plaintext( async fn server_plaintext_loop(
addr: SocketAddr, listener: TcpListener,
app: IntoMakeServiceWithConnectInfo<Router, ClientConnInfo>, app: IntoMakeServiceWithConnectInfo<Router, ClientConnInfo>,
mut rx: broadcast::Receiver<CoreAction>, mut rx: broadcast::Receiver<CoreAction>,
trusted_proxy_v2_ips: Option<Arc<HashSet<IpAddr>>>,
) { ) {
let listener = axum_server::bind(addr).serve(app);
pin_mut!(listener); pin_mut!(listener);
loop { loop {
tokio::select! { tokio::select! {
Ok(action) = rx.recv() => { Ok(action) = rx.recv() => {
match action { match action {
CoreAction::Shutdown => CoreAction::Shutdown => break,
break, }
}
accept = listener.accept() => {
match accept {
Ok((stream, addr)) => {
let app = app.clone();
task::spawn(handle_conn(stream, app, addr, trusted_proxy_v2_ips.clone()));
}
Err(err) => {
error!("Web server exited with {:?}", err);
break;
}
} }
} }
_ = &mut listener => {}
} }
} }
@ -412,11 +435,38 @@ async fn server_loop_plaintext(
/// This handles an individual connection. /// This handles an individual connection.
pub(crate) async fn handle_conn( pub(crate) async fn handle_conn(
stream: TcpStream,
app: IntoMakeServiceWithConnectInfo<Router, ClientConnInfo>,
connection_addr: SocketAddr,
trusted_proxy_v2_ips: Option<Arc<HashSet<IpAddr>>>,
) -> Result<(), std::io::Error> {
let (stream, client_addr) =
process_client_addr(stream, connection_addr, trusted_proxy_v2_ips).await?;
let client_conn_info = ClientConnInfo {
connection_addr,
client_addr,
client_cert: None,
};
// Hyper has its own `AsyncRead` and `AsyncWrite` traits and doesn't use tokio.
// `TokioIo` converts between them.
let stream = TokioIo::new(stream);
process_client_hyper(stream, app, client_conn_info).await
}
/// This handles an individual connection.
pub(crate) async fn handle_tls_conn(
acceptor: SslAcceptor, acceptor: SslAcceptor,
stream: TcpStream, stream: TcpStream,
mut app: IntoMakeServiceWithConnectInfo<Router, ClientConnInfo>, app: IntoMakeServiceWithConnectInfo<Router, ClientConnInfo>,
addr: SocketAddr, connection_addr: SocketAddr,
trusted_proxy_v2_ips: Option<Arc<HashSet<IpAddr>>>,
) -> Result<(), std::io::Error> { ) -> Result<(), std::io::Error> {
let (stream, client_addr) =
process_client_addr(stream, connection_addr, trusted_proxy_v2_ips).await?;
let ssl = Ssl::new(acceptor.context()).map_err(|e| { let ssl = Ssl::new(acceptor.context()).map_err(|e| {
error!("Failed to create TLS context: {:?}", e); error!("Failed to create TLS context: {:?}", e);
std::io::Error::from(ErrorKind::ConnectionAborted) std::io::Error::from(ErrorKind::ConnectionAborted)
@ -459,42 +509,17 @@ pub(crate) async fn handle_conn(
None None
}; };
let client_conn_info = ClientConnInfo { addr, client_cert }; let client_conn_info = ClientConnInfo {
connection_addr,
debug!(?client_conn_info); client_addr,
client_cert,
let svc = axum_server::service::MakeService::<ClientConnInfo, hyper::Request<Body>>::make_service( };
&mut app,
client_conn_info,
);
let svc = svc.await.map_err(|e| {
error!("Failed to build HTTP response: {:?}", e);
std::io::Error::from(ErrorKind::Other)
})?;
// Hyper has its own `AsyncRead` and `AsyncWrite` traits and doesn't use tokio. // Hyper has its own `AsyncRead` and `AsyncWrite` traits and doesn't use tokio.
// `TokioIo` converts between them. // `TokioIo` converts between them.
let stream = TokioIo::new(tls_stream); let stream = TokioIo::new(tls_stream);
// Hyper also has its own `Service` trait and doesn't use tower. We can use process_client_hyper(stream, app, client_conn_info).await
// `hyper::service::service_fn` to create a hyper `Service` that calls our app through
// `tower::Service::call`.
let hyper_service = hyper::service::service_fn(move |request: Request<Incoming>| {
// We have to clone `tower_service` because hyper's `Service` uses `&self` whereas
// tower's `Service` requires `&mut self`.
//
// We don't need to call `poll_ready` since `Router` is always ready.
svc.clone().call(request)
});
hyper_util::server::conn::auto::Builder::new(TokioExecutor::new())
.serve_connection_with_upgrades(stream, hyper_service)
.await
.map_err(|e| {
debug!("Failed to complete connection: {:?}", e);
std::io::Error::from(ErrorKind::ConnectionAborted)
})
} }
Err(error) => { Err(error) => {
trace!("Failed to handle connection: {:?}", error); trace!("Failed to handle connection: {:?}", error);
@ -502,3 +527,83 @@ pub(crate) async fn handle_conn(
} }
} }
} }
async fn process_client_addr(
stream: TcpStream,
connection_addr: SocketAddr,
trusted_proxy_v2_ips: Option<Arc<HashSet<IpAddr>>>,
) -> Result<(TcpStream, SocketAddr), std::io::Error> {
let enable_proxy_v2_hdr = trusted_proxy_v2_ips
.map(|trusted| trusted.contains(&connection_addr.ip()))
.unwrap_or_default();
let (stream, client_addr) = if enable_proxy_v2_hdr {
match ProxyHdrV2::parse_from_read(stream).await {
Ok((stream, hdr)) => {
let remote_socket_addr = match hdr.to_remote_addr() {
RemoteAddress::Local => {
debug!("PROXY protocol liveness check - will not contain client data");
return Err(std::io::Error::from(ErrorKind::ConnectionAborted));
}
RemoteAddress::TcpV4 { src, dst: _ } => SocketAddr::from(src),
RemoteAddress::TcpV6 { src, dst: _ } => SocketAddr::from(src),
remote_addr => {
error!(?remote_addr, "remote address in proxy header is invalid");
return Err(std::io::Error::from(ErrorKind::ConnectionAborted));
}
};
(stream, remote_socket_addr)
}
Err(err) => {
error!(?connection_addr, ?err, "Unable to process proxy v2 header");
return Err(std::io::Error::from(ErrorKind::ConnectionAborted));
}
}
} else {
(stream, connection_addr)
};
Ok((stream, client_addr))
}
async fn process_client_hyper<T>(
stream: TokioIo<T>,
mut app: IntoMakeServiceWithConnectInfo<Router, ClientConnInfo>,
client_conn_info: ClientConnInfo,
) -> Result<(), std::io::Error>
where
T: AsyncRead + AsyncWrite + std::marker::Unpin + std::marker::Send + 'static,
{
debug!(?client_conn_info);
let svc =
axum_server::service::MakeService::<ClientConnInfo, hyper::Request<Body>>::make_service(
&mut app,
client_conn_info,
);
let svc = svc.await.map_err(|e| {
error!("Failed to build HTTP response: {:?}", e);
std::io::Error::from(ErrorKind::Other)
})?;
// Hyper also has its own `Service` trait and doesn't use tower. We can use
// `hyper::service::service_fn` to create a hyper `Service` that calls our app through
// `tower::Service::call`.
let hyper_service = hyper::service::service_fn(move |request: Request<Incoming>| {
// We have to clone `tower_service` because hyper's `Service` uses `&self` whereas
// tower's `Service` requires `&mut self`.
//
// We don't need to call `poll_ready` since `Router` is always ready.
svc.clone().call(request)
});
hyper_util::server::conn::auto::Builder::new(TokioExecutor::new())
.serve_connection_with_upgrades(stream, hyper_service)
.await
.map_err(|e| {
debug!("Failed to complete connection: {:?}", e);
std::io::Error::from(ErrorKind::ConnectionAborted)
})
}

View file

@ -2,14 +2,17 @@ use crate::actors::QueryServerReadV1;
use crate::CoreAction; use crate::CoreAction;
use futures_util::sink::SinkExt; use futures_util::sink::SinkExt;
use futures_util::stream::StreamExt; use futures_util::stream::StreamExt;
use haproxy_protocol::{ProxyHdrV2, RemoteAddress};
use hashbrown::HashSet;
use kanidmd_lib::idm::ldap::{LdapBoundToken, LdapResponseState}; use kanidmd_lib::idm::ldap::{LdapBoundToken, LdapResponseState};
use kanidmd_lib::prelude::*; use kanidmd_lib::prelude::*;
use ldap3_proto::proto::LdapMsg; use ldap3_proto::proto::LdapMsg;
use ldap3_proto::LdapCodec; use ldap3_proto::LdapCodec;
use openssl::ssl::{Ssl, SslAcceptor}; use openssl::ssl::{Ssl, SslAcceptor};
use std::net; use std::net::{IpAddr, SocketAddr};
use std::pin::Pin; use std::pin::Pin;
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc;
use tokio::io::{AsyncRead, AsyncWrite}; use tokio::io::{AsyncRead, AsyncWrite};
use tokio::net::{TcpListener, TcpStream}; use tokio::net::{TcpListener, TcpStream};
use tokio::sync::broadcast; use tokio::sync::broadcast;
@ -33,7 +36,7 @@ impl LdapSession {
#[instrument(name = "ldap-request", skip(client_address, qe_r_ref))] #[instrument(name = "ldap-request", skip(client_address, qe_r_ref))]
async fn client_process_msg( async fn client_process_msg(
uat: Option<LdapBoundToken>, uat: Option<LdapBoundToken>,
client_address: net::SocketAddr, client_address: SocketAddr,
protomsg: LdapMsg, protomsg: LdapMsg,
qe_r_ref: &'static QueryServerReadV1, qe_r_ref: &'static QueryServerReadV1,
) -> Option<LdapResponseState> { ) -> Option<LdapResponseState> {
@ -50,7 +53,8 @@ async fn client_process_msg(
async fn client_process<STREAM>( async fn client_process<STREAM>(
stream: STREAM, stream: STREAM,
client_address: net::SocketAddr, client_address: SocketAddr,
connection_address: SocketAddr,
qe_r_ref: &'static QueryServerReadV1, qe_r_ref: &'static QueryServerReadV1,
) where ) where
STREAM: AsyncRead + AsyncWrite, STREAM: AsyncRead + AsyncWrite,
@ -67,6 +71,8 @@ async fn client_process<STREAM>(
let uat = session.uat.clone(); let uat = session.uat.clone();
let caddr = client_address; let caddr = client_address;
debug!(?client_address, ?connection_address);
match client_process_msg(uat, caddr, protomsg, qe_r_ref).await { match client_process_msg(uat, caddr, protomsg, qe_r_ref).await {
// I'd really have liked to have put this near the [LdapResponseState::Bind] but due // I'd really have liked to have put this near the [LdapResponseState::Bind] but due
// to the handing of `audit` it isn't possible due to borrows, etc. // to the handing of `audit` it isn't possible due to borrows, etc.
@ -112,28 +118,65 @@ async fn client_process<STREAM>(
} }
async fn client_tls_accept( async fn client_tls_accept(
tcpstream: TcpStream, stream: TcpStream,
tls_acceptor: SslAcceptor, tls_acceptor: SslAcceptor,
client_socket_addr: net::SocketAddr, connection_addr: SocketAddr,
qe_r_ref: &'static QueryServerReadV1, qe_r_ref: &'static QueryServerReadV1,
trusted_proxy_v2_ips: Option<Arc<HashSet<IpAddr>>>,
) { ) {
let enable_proxy_v2_hdr = trusted_proxy_v2_ips
.map(|trusted| trusted.contains(&connection_addr.ip()))
.unwrap_or_default();
let (stream, client_addr) = if enable_proxy_v2_hdr {
match ProxyHdrV2::parse_from_read(stream).await {
Ok((stream, hdr)) => {
let remote_socket_addr = match hdr.to_remote_addr() {
RemoteAddress::Local => {
debug!("PROXY protocol liveness check - will not contain client data");
return;
}
RemoteAddress::TcpV4 { src, dst: _ } => SocketAddr::from(src),
RemoteAddress::TcpV6 { src, dst: _ } => SocketAddr::from(src),
remote_addr => {
error!(?remote_addr, "remote address in proxy header is invalid");
return;
}
};
(stream, remote_socket_addr)
}
Err(err) => {
error!(?connection_addr, ?err, "Unable to process proxy v2 header");
return;
}
}
} else {
(stream, connection_addr)
};
// Start the event // Start the event
// From the parameters we need to create an SslContext. // From the parameters we need to create an SslContext.
let mut tlsstream = match Ssl::new(tls_acceptor.context()) let mut tlsstream = match Ssl::new(tls_acceptor.context())
.and_then(|tls_obj| SslStream::new(tls_obj, tcpstream)) .and_then(|tls_obj| SslStream::new(tls_obj, stream))
{ {
Ok(ta) => ta, Ok(ta) => ta,
Err(err) => { Err(err) => {
error!(?err, %client_socket_addr, "LDAP TLS setup error"); error!(?err, %client_addr, %connection_addr, "LDAP TLS setup error");
return; return;
} }
}; };
if let Err(err) = SslStream::accept(Pin::new(&mut tlsstream)).await { if let Err(err) = SslStream::accept(Pin::new(&mut tlsstream)).await {
error!(?err, %client_socket_addr, "LDAP TLS accept error"); error!(?err, %client_addr, %connection_addr, "LDAP TLS accept error");
return; return;
}; };
tokio::spawn(client_process(tlsstream, client_socket_addr, qe_r_ref)); tokio::spawn(client_process(
tlsstream,
client_addr,
connection_addr,
qe_r_ref,
));
} }
/// TLS LDAP Listener, hands off to [client_tls_accept] /// TLS LDAP Listener, hands off to [client_tls_accept]
@ -143,6 +186,7 @@ async fn ldap_tls_acceptor(
qe_r_ref: &'static QueryServerReadV1, qe_r_ref: &'static QueryServerReadV1,
mut rx: broadcast::Receiver<CoreAction>, mut rx: broadcast::Receiver<CoreAction>,
mut tls_acceptor_reload_rx: mpsc::Receiver<SslAcceptor>, mut tls_acceptor_reload_rx: mpsc::Receiver<SslAcceptor>,
trusted_proxy_v2_ips: Option<Arc<HashSet<IpAddr>>>,
) { ) {
loop { loop {
tokio::select! { tokio::select! {
@ -155,7 +199,7 @@ async fn ldap_tls_acceptor(
match accept_result { match accept_result {
Ok((tcpstream, client_socket_addr)) => { Ok((tcpstream, client_socket_addr)) => {
let clone_tls_acceptor = tls_acceptor.clone(); let clone_tls_acceptor = tls_acceptor.clone();
tokio::spawn(client_tls_accept(tcpstream, clone_tls_acceptor, client_socket_addr, qe_r_ref)); tokio::spawn(client_tls_accept(tcpstream, clone_tls_acceptor, client_socket_addr, qe_r_ref, trusted_proxy_v2_ips.clone()));
} }
Err(err) => { Err(err) => {
warn!(?err, "LDAP acceptor error, continuing"); warn!(?err, "LDAP acceptor error, continuing");
@ -187,7 +231,7 @@ async fn ldap_plaintext_acceptor(
accept_result = listener.accept() => { accept_result = listener.accept() => {
match accept_result { match accept_result {
Ok((tcpstream, client_socket_addr)) => { Ok((tcpstream, client_socket_addr)) => {
tokio::spawn(client_process(tcpstream, client_socket_addr, qe_r_ref)); tokio::spawn(client_process(tcpstream, client_socket_addr, client_socket_addr, qe_r_ref));
} }
Err(e) => { Err(e) => {
error!("LDAP acceptor error, continuing -> {:?}", e); error!("LDAP acceptor error, continuing -> {:?}", e);
@ -205,6 +249,7 @@ pub(crate) async fn create_ldap_server(
qe_r_ref: &'static QueryServerReadV1, qe_r_ref: &'static QueryServerReadV1,
rx: broadcast::Receiver<CoreAction>, rx: broadcast::Receiver<CoreAction>,
tls_acceptor_reload_rx: mpsc::Receiver<SslAcceptor>, tls_acceptor_reload_rx: mpsc::Receiver<SslAcceptor>,
trusted_proxy_v2_ips: Option<HashSet<IpAddr>>,
) -> Result<tokio::task::JoinHandle<()>, ()> { ) -> Result<tokio::task::JoinHandle<()>, ()> {
if address.starts_with(":::") { if address.starts_with(":::") {
// takes :::xxxx to xxxx // takes :::xxxx to xxxx
@ -212,7 +257,7 @@ pub(crate) async fn create_ldap_server(
error!("Address '{}' looks like an attempt to wildcard bind with IPv6 on port {} - please try using ldapbindaddress = '[::]:{}'", address, port, port); error!("Address '{}' looks like an attempt to wildcard bind with IPv6 on port {} - please try using ldapbindaddress = '[::]:{}'", address, port, port);
}; };
let addr = net::SocketAddr::from_str(address).map_err(|e| { let addr = SocketAddr::from_str(address).map_err(|e| {
error!("Could not parse LDAP server address {} -> {:?}", address, e); error!("Could not parse LDAP server address {} -> {:?}", address, e);
})?; })?;
@ -223,6 +268,8 @@ pub(crate) async fn create_ldap_server(
); );
})?; })?;
let trusted_proxy_v2_ips = trusted_proxy_v2_ips.map(Arc::new);
let ldap_acceptor_handle = match opt_ssl_acceptor { let ldap_acceptor_handle = match opt_ssl_acceptor {
Some(ssl_acceptor) => { Some(ssl_acceptor) => {
info!("Starting LDAPS interface ldaps://{} ...", address); info!("Starting LDAPS interface ldaps://{} ...", address);
@ -233,6 +280,7 @@ pub(crate) async fn create_ldap_server(
qe_r_ref, qe_r_ref,
rx, rx,
tls_acceptor_reload_rx, tls_acceptor_reload_rx,
trusted_proxy_v2_ips,
)) ))
} }
None => tokio::spawn(ldap_plaintext_acceptor(listener, qe_r_ref, rx)), None => tokio::spawn(ldap_plaintext_acceptor(listener, qe_r_ref, rx)),

View file

@ -1087,6 +1087,7 @@ pub async fn create_server_core(
server_read_ref, server_read_ref,
broadcast_tx.subscribe(), broadcast_tx.subscribe(),
ldap_tls_acceptor_reload_rx, ldap_tls_acceptor_reload_rx,
config.ldap_client_address_info.trusted_proxy_v2(),
) )
.await?; .await?;
Some(h) Some(h)

View file

@ -2417,7 +2417,7 @@ mod tests {
let lims = Limits::unlimited(); let lims = Limits::unlimited();
let r = be.search(&lims, &filt); let r = be.search(&lims, &filt);
assert!(r.expect("Search failed!").len() == 0); assert!(r.expect("Search failed!").is_empty());
}); });
} }

View file

@ -145,8 +145,8 @@ impl Totp {
// Create a new token with secure key and algo. // Create a new token with secure key and algo.
pub fn generate_secure(step: u64) -> Self { pub fn generate_secure(step: u64) -> Self {
let mut rng = rand::thread_rng(); let mut rng = rand::rng();
let secret: Vec<u8> = (0..SECRET_SIZE_BYTES).map(|_| rng.gen()).collect(); let secret: Vec<u8> = (0..SECRET_SIZE_BYTES).map(|_| rng.random()).collect();
let algo = TotpAlgo::Sha256; let algo = TotpAlgo::Sha256;
let digits = TotpDigits::Six; let digits = TotpDigits::Six;
Totp { Totp {

View file

@ -17,6 +17,7 @@ use webauthn_rs::prelude::{
AttestedPasskey as AttestedPasskeyV4, AttestedPasskeyRegistration, CreationChallengeResponse, AttestedPasskey as AttestedPasskeyV4, AttestedPasskeyRegistration, CreationChallengeResponse,
Passkey as PasskeyV4, PasskeyRegistration, RegisterPublicKeyCredential, WebauthnError, Passkey as PasskeyV4, PasskeyRegistration, RegisterPublicKeyCredential, WebauthnError,
}; };
use zxcvbn::{zxcvbn, Score};
use crate::credential::totp::{Totp, TOTP_DEFAULT_STEP}; use crate::credential::totp::{Totp, TOTP_DEFAULT_STEP};
use crate::credential::{BackupCodes, Credential}; use crate::credential::{BackupCodes, Credential};
@ -1663,23 +1664,14 @@ impl IdmServerCredUpdateTransaction<'_> {
} }
// does the password pass zxcvbn? // does the password pass zxcvbn?
let entropy = zxcvbn::zxcvbn(cleartext, related_inputs).map_err(|e| { let entropy = zxcvbn(cleartext, related_inputs);
admin_error!("zxcvbn check failure (password empty?) {:?}", e);
// Return some generic feedback when the password is this bad.
PasswordQuality::Feedback(vec![
PasswordFeedback::UseAFewWordsAvoidCommonPhrases,
PasswordFeedback::AddAnotherWordOrTwo,
PasswordFeedback::NoNeedForSymbolsDigitsOrUppercaseLetters,
])
})?;
// PW's should always be enforced as strong as possible. // PW's should always be enforced as strong as possible.
if entropy.score() < 4 { if entropy.score() < Score::Four {
// The password is too week as per: // The password is too week as per:
// https://docs.rs/zxcvbn/2.0.0/zxcvbn/struct.Entropy.html // https://docs.rs/zxcvbn/2.0.0/zxcvbn/struct.Entropy.html
let feedback: zxcvbn::feedback::Feedback = entropy let feedback: zxcvbn::feedback::Feedback = entropy
.feedback() .feedback()
.as_ref()
.ok_or(OperationError::InvalidState) .ok_or(OperationError::InvalidState)
.cloned() .cloned()
.map_err(|e| { .map_err(|e| {
@ -3405,7 +3397,7 @@ mod tests {
assert!( assert!(
matches!( matches!(
c_status.mfaregstate, c_status.mfaregstate,
MfaRegStateStatus::TotpNameTryAgain(ref val) if val == "" MfaRegStateStatus::TotpNameTryAgain(ref val) if val.is_empty()
), ),
"{:?}", "{:?}",
c_status.mfaregstate c_status.mfaregstate

View file

@ -1209,7 +1209,7 @@ mod tests {
// Searching a malformed spn shouldn't cause the query to fail // Searching a malformed spn shouldn't cause the query to fail
let sr = SearchRequest { let sr = SearchRequest {
msgid: 1, msgid: 1,
base: format!("dc=example,dc=com"), base: "dc=example,dc=com".to_string(),
scope: LdapSearchScope::Subtree, scope: LdapSearchScope::Subtree,
filter: LdapFilter::Or(vec![ filter: LdapFilter::Or(vec![
LdapFilter::Equality(Attribute::Name.to_string(), usr_name.to_string()), LdapFilter::Equality(Attribute::Name.to_string(), usr_name.to_string()),
@ -1232,7 +1232,7 @@ mod tests {
let sr = SearchRequest { let sr = SearchRequest {
msgid: 1, msgid: 1,
base: format!("dc=example,dc=com"), base: "dc=example,dc=com".to_string(),
scope: LdapSearchScope::Subtree, scope: LdapSearchScope::Subtree,
filter: LdapFilter::And(vec![ filter: LdapFilter::And(vec![
LdapFilter::Equality(Attribute::Name.to_string(), usr_name.to_string()), LdapFilter::Equality(Attribute::Name.to_string(), usr_name.to_string()),

View file

@ -2992,11 +2992,12 @@ fn validate_scopes(req_scopes: &BTreeSet<String>) -> Result<(), Oauth2Error> {
#[cfg(any(feature = "dev-oauth2-device-flow", test))] #[cfg(any(feature = "dev-oauth2-device-flow", test))]
#[allow(dead_code)] #[allow(dead_code)]
fn gen_device_code() -> Result<[u8; 16], Oauth2Error> { fn gen_device_code() -> Result<[u8; 16], Oauth2Error> {
let mut rng = rand::thread_rng(); use rand::TryRngCore;
let mut rng = rand::rng();
let mut result = [0u8; 16]; let mut result = [0u8; 16];
// doing it here because of feature-shenanigans. // doing it here because of feature-shenanigans.
use rand::Rng; if let Err(err) = rng.try_fill_bytes(&mut result) {
if let Err(err) = rng.try_fill(&mut result) {
error!("Failed to generate device code! {:?}", err); error!("Failed to generate device code! {:?}", err);
return Err(Oauth2Error::ServerError(OperationError::Backend)); return Err(Oauth2Error::ServerError(OperationError::Backend));
} }
@ -3009,8 +3010,8 @@ fn gen_device_code() -> Result<[u8; 16], Oauth2Error> {
/// Returns (xxx-yyy-zzz, digits) where one's the human-facing code, the other is what we store in the DB. /// Returns (xxx-yyy-zzz, digits) where one's the human-facing code, the other is what we store in the DB.
fn gen_user_code() -> (String, u32) { fn gen_user_code() -> (String, u32) {
use rand::Rng; use rand::Rng;
let mut rng = rand::thread_rng(); let mut rng = rand::rng();
let num: u32 = rng.gen_range(0..=999999999); let num: u32 = rng.random_range(0..=999999999);
let result = format!("{:09}", num); let result = format!("{:09}", num);
( (
format!("{}-{}-{}", &result[0..3], &result[3..6], &result[6..9]), format!("{}-{}-{}", &result[0..3], &result[3..6], &result[6..9]),
@ -3100,6 +3101,7 @@ mod tests {
$code_challenge:expr, $code_challenge:expr,
$scope:expr $scope:expr
) => {{ ) => {{
#[allow(clippy::unnecessary_to_owned)]
let scope: BTreeSet<String> = $scope.split(" ").map(|s| s.to_string()).collect(); let scope: BTreeSet<String> = $scope.split(" ").map(|s| s.to_string()).collect();
let auth_req = AuthorisationRequest { let auth_req = AuthorisationRequest {
@ -7312,10 +7314,7 @@ mod tests {
&Url::parse(example_is_not_local) &Url::parse(example_is_not_local)
.expect("Failed to parse example.com as a host?") .expect("Failed to parse example.com as a host?")
.host() .host()
.expect(&format!( .unwrap_or_else(|| panic!("Couldn't get a host from {}", example_is_not_local))
"Couldn't get a host from {}",
example_is_not_local
))
)); ));
let test_urls = [ let test_urls = [

View file

@ -21,6 +21,7 @@ use tokio::sync::{Mutex, Semaphore};
use tracing::trace; use tracing::trace;
use url::Url; use url::Url;
use webauthn_rs::prelude::{Webauthn, WebauthnBuilder}; use webauthn_rs::prelude::{Webauthn, WebauthnBuilder};
use zxcvbn::{zxcvbn, Score};
use super::event::ReadBackupCodeEvent; use super::event::ReadBackupCodeEvent;
use super::ldap::{LdapBoundToken, LdapSession}; use super::ldap::{LdapBoundToken, LdapSession};
@ -235,7 +236,7 @@ impl IdmServer {
let qs_read = self.qs.read().await?; let qs_read = self.qs.read().await?;
let mut sid = [0; 4]; let mut sid = [0; 4];
let mut rng = StdRng::from_entropy(); let mut rng = StdRng::from_os_rng();
rng.fill(&mut sid); rng.fill(&mut sid);
Ok(IdmServerAuthTransaction { Ok(IdmServerAuthTransaction {
@ -278,7 +279,7 @@ impl IdmServer {
let qs_write = self.qs.write(ts).await?; let qs_write = self.qs.write(ts).await?;
let mut sid = [0; 4]; let mut sid = [0; 4];
let mut rng = StdRng::from_entropy(); let mut rng = StdRng::from_os_rng();
rng.fill(&mut sid); rng.fill(&mut sid);
Ok(IdmServerProxyWriteTransaction { Ok(IdmServerProxyWriteTransaction {
@ -1657,18 +1658,14 @@ impl IdmServerProxyWriteTransaction<'_> {
// does the password pass zxcvbn? // does the password pass zxcvbn?
let entropy = zxcvbn::zxcvbn(cleartext, related_inputs).map_err(|e| { let entropy = zxcvbn(cleartext, related_inputs);
admin_error!("zxcvbn check failure (password empty?) {:?}", e);
OperationError::PasswordQuality(vec![PasswordFeedback::TooShort(PW_MIN_LENGTH)])
})?;
// Unix PW's are a single factor, so we enforce good pws // Unix PW's are a single factor, so we enforce good pws
if entropy.score() < 4 { if entropy.score() < Score::Four {
// The password is too week as per: // The password is too week as per:
// https://docs.rs/zxcvbn/2.0.0/zxcvbn/struct.Entropy.html // https://docs.rs/zxcvbn/2.0.0/zxcvbn/struct.Entropy.html
let feedback: zxcvbn::feedback::Feedback = entropy let feedback: zxcvbn::feedback::Feedback = entropy
.feedback() .feedback()
.as_ref()
.ok_or(OperationError::InvalidState) .ok_or(OperationError::InvalidState)
.cloned() .cloned()
.inspect_err(|err| { .inspect_err(|err| {

View file

@ -217,9 +217,9 @@ impl EntryChangeState {
} }
#[cfg(test)] #[cfg(test)]
pub(crate) fn get_attr_cid(&self, attr: Attribute) -> Option<&Cid> { pub(crate) fn get_attr_cid(&self, attr: &Attribute) -> Option<&Cid> {
match &self.st { match &self.st {
State::Live { at: _, changes } => changes.get(&attr), State::Live { at: _, changes } => changes.get(attr),
State::Tombstone { at: _ } => None, State::Tombstone { at: _ } => None,
} }
} }

View file

@ -705,7 +705,7 @@ async fn test_repl_increment_basic_deleted_attr(server_a: &QueryServer, server_b
let e1_cs = e1.get_changestate(); let e1_cs = e1.get_changestate();
let e2_cs = e2.get_changestate(); let e2_cs = e2.get_changestate();
assert_eq!(e1_cs, e2_cs); assert_eq!(e1_cs, e2_cs);
assert!(e1_cs.get_attr_cid(Attribute::Description).is_some()); assert!(e1_cs.get_attr_cid(&Attribute::Description).is_some());
server_b_txn.commit().expect("Failed to commit"); server_b_txn.commit().expect("Failed to commit");
drop(server_a_txn); drop(server_a_txn);

View file

@ -186,7 +186,7 @@ mod tests {
match desc { match desc {
ScimValueKanidm::String(gdesc) if gdesc == "Group Description" => {} ScimValueKanidm::String(gdesc) if gdesc == "Group Description" => {}
_ => assert!(false), _ => unreachable!("Expected a string"),
}; };
// null removes attr // null removes attr
@ -201,7 +201,7 @@ mod tests {
.expect("Failed to resolve data type"); .expect("Failed to resolve data type");
let updated_entry = server_txn.scim_put(put_event).expect("Failed to put"); let updated_entry = server_txn.scim_put(put_event).expect("Failed to put");
assert!(updated_entry.attrs.get(&Attribute::Description).is_none()); assert!(!updated_entry.attrs.contains_key(&Attribute::Description));
// set one // set one
let put = ScimEntryPutKanidm { let put = ScimEntryPutKanidm {
@ -234,7 +234,7 @@ mod tests {
value: "extra_1@example.com".to_string(), value: "extra_1@example.com".to_string(),
})); }));
} }
_ => assert!(false), _ => unreachable!("Expected 1 member"),
}; };
// set many // set many
@ -285,7 +285,7 @@ mod tests {
value: "extra_3@example.com".to_string(), value: "extra_3@example.com".to_string(),
})); }));
} }
_ => assert!(false), _ => unreachable!("Expected 3 members"),
}; };
// set many with a removal // set many with a removal
@ -333,7 +333,7 @@ mod tests {
value: "extra_2@example.com".to_string(), value: "extra_2@example.com".to_string(),
})); }));
} }
_ => assert!(false), _ => unreachable!("Expected 2 members"),
}; };
// empty set removes attr // empty set removes attr
@ -348,6 +348,6 @@ mod tests {
.expect("Failed to resolve data type"); .expect("Failed to resolve data type");
let updated_entry = server_txn.scim_put(put_event).expect("Failed to put"); let updated_entry = server_txn.scim_put(put_event).expect("Failed to put");
assert!(updated_entry.attrs.get(&Attribute::Member).is_none()); assert!(!updated_entry.attrs.contains_key(&Attribute::Member));
} }
} }

View file

@ -4,12 +4,19 @@ use crate::schema::Schema;
pub struct TestConfiguration { pub struct TestConfiguration {
pub domain_level: DomainVersion, pub domain_level: DomainVersion,
// This is literally here to make clippy happy, just leave it alone!
// if you don't believe me then remove it and run 'cargo clippy --all-targets' it'll complain
// about "struct update has no effect, all the fields in the struct have already been specified"
// because the domain_level was set, then we ..Default::default() the "rest"
#[allow(dead_code)]
pub ignore_this_field: bool,
} }
impl Default for TestConfiguration { impl Default for TestConfiguration {
fn default() -> Self { fn default() -> Self {
TestConfiguration { TestConfiguration {
domain_level: DOMAIN_TGT_LEVEL, domain_level: DOMAIN_TGT_LEVEL,
ignore_this_field: false,
} }
} }
} }

View file

@ -2,8 +2,8 @@
use crate::prelude::*; use crate::prelude::*;
use hashbrown::HashSet; use hashbrown::HashSet;
use rand::distributions::{Distribution, Uniform}; use rand::distr::{Distribution, Uniform};
use rand::{thread_rng, Rng}; use rand::{rng, Rng};
use std::ops::Range; use std::ops::Range;
#[derive(Debug)] #[derive(Debug)]
@ -35,7 +35,7 @@ pub fn uuid_from_duration(d: Duration, sid: Sid) -> Uuid {
} }
pub(crate) fn password_from_random_len(len: u32) -> String { pub(crate) fn password_from_random_len(len: u32) -> String {
thread_rng() rng()
.sample_iter(&DistinctAlpha) .sample_iter(&DistinctAlpha)
.take(len as usize) .take(len as usize)
.collect::<String>() .collect::<String>()
@ -52,7 +52,7 @@ pub fn backup_code_from_random() -> HashSet<String> {
pub fn readable_password_from_random() -> String { pub fn readable_password_from_random() -> String {
// 2^112 bits, means we need at least 55^20 to have as many bits of entropy. // 2^112 bits, means we need at least 55^20 to have as many bits of entropy.
// this leads us to 4 groups of 5 to create 55^20 // this leads us to 4 groups of 5 to create 55^20
let mut trng = thread_rng(); let mut trng = rng();
format!( format!(
"{}-{}-{}-{}", "{}-{}-{}-{}",
(&mut trng) (&mut trng)
@ -80,8 +80,9 @@ impl Distribution<char> for DistinctAlpha {
const GEN_ASCII_STR_CHARSET: &[u8] = b"ABCDEFGHJKLMNPQRSTUVWXYZ\ const GEN_ASCII_STR_CHARSET: &[u8] = b"ABCDEFGHJKLMNPQRSTUVWXYZ\
abcdefghjkpqrstuvwxyz\ abcdefghjkpqrstuvwxyz\
0123456789"; 0123456789";
// TODO: this needs to handle the error, maybe?
let range = Uniform::new(0, RANGE); #[allow(clippy::expect_used)]
let range = Uniform::new(0, RANGE).expect("Failed to get a uniform range");
let n = range.sample(rng); let n = range.sample(rng);
GEN_ASCII_STR_CHARSET[n as usize] as char GEN_ASCII_STR_CHARSET[n as usize] as char

View file

@ -683,10 +683,10 @@ mod tests {
"value": "claire@example.com" "value": "claire@example.com"
} }
]"#; ]"#;
crate::valueset::scim_json_reflexive(vs.clone(), data); crate::valueset::scim_json_reflexive(&vs, data);
// Test that we can parse json values into a valueset. // Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive::<ValueSetEmailAddress>(vs, &[]) crate::valueset::scim_json_put_reflexive::<ValueSetEmailAddress>(&vs, &[])
} }
#[test] #[test]
@ -711,9 +711,9 @@ mod tests {
} }
]"#; ]"#;
crate::valueset::scim_json_reflexive(vs.clone(), data); crate::valueset::scim_json_reflexive(&vs, data);
// Test that we can parse json values into a valueset. // Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive::<ValueSetAddress>(vs, &[]) crate::valueset::scim_json_put_reflexive::<ValueSetAddress>(&vs, &[])
} }
} }

View file

@ -381,6 +381,6 @@ mod tests {
} }
] ]
"#; "#;
crate::valueset::scim_json_reflexive(vs, data); crate::valueset::scim_json_reflexive(&vs, data);
} }
} }

View file

@ -400,6 +400,6 @@ mod tests {
} }
] ]
"#; "#;
crate::valueset::scim_json_reflexive(vs, data); crate::valueset::scim_json_reflexive(&vs, data);
} }
} }

View file

@ -185,9 +185,9 @@ mod tests {
#[test] #[test]
fn test_scim_boolean() { fn test_scim_boolean() {
let vs: ValueSet = ValueSetBool::new(true); let vs: ValueSet = ValueSetBool::new(true);
crate::valueset::scim_json_reflexive(vs.clone(), "true"); crate::valueset::scim_json_reflexive(&vs, "true");
// Test that we can parse json values into a valueset. // Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive::<ValueSetBool>(vs, &[]) crate::valueset::scim_json_put_reflexive::<ValueSetBool>(&vs, &[])
} }
} }

View file

@ -352,6 +352,6 @@ raBy6edj7W0EIH+yQxkDEwIhAI0nVKaI6duHLAvtKW6CfEQFG6jKg7dyk37YYiRD
assert_eq!(cert.s256, expect_s256); assert_eq!(cert.s256, expect_s256);
// Test that we can parse json values into a valueset. // Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive::<ValueSetCertificate>(vs, &[]) crate::valueset::scim_json_put_reflexive::<ValueSetCertificate>(&vs, &[])
} }
} }

View file

@ -193,6 +193,6 @@ mod tests {
let vs: ValueSet = ValueSetCid::new(Cid::new_zero()); let vs: ValueSet = ValueSetCid::new(Cid::new_zero());
let data = r#""00000000000000000000000000000000-00000000-0000-0000-0000-000000000000""#; let data = r#""00000000000000000000000000000000-00000000-0000-0000-0000-000000000000""#;
crate::valueset::scim_json_reflexive(vs, data); crate::valueset::scim_json_reflexive(&vs, data);
} }
} }

View file

@ -1183,15 +1183,15 @@ mod tests {
} }
] ]
"#; "#;
crate::valueset::scim_json_reflexive(vs, data); crate::valueset::scim_json_reflexive(&vs, data);
} }
#[test] #[test]
fn test_scim_credential_type() { fn test_scim_credential_type() {
let vs: ValueSet = ValueSetCredentialType::new(CredentialType::Mfa); let vs: ValueSet = ValueSetCredentialType::new(CredentialType::Mfa);
crate::valueset::scim_json_reflexive(vs.clone(), r#""mfa""#); crate::valueset::scim_json_reflexive(&vs, r#""mfa""#);
// Test that we can parse json values into a valueset. // Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive::<ValueSetCredentialType>(vs, &[]) crate::valueset::scim_json_put_reflexive::<ValueSetCredentialType>(&vs, &[])
} }
} }

View file

@ -210,9 +210,9 @@ mod tests {
let odt = OffsetDateTime::UNIX_EPOCH + Duration::from_secs(69_420); let odt = OffsetDateTime::UNIX_EPOCH + Duration::from_secs(69_420);
let vs: ValueSet = ValueSetDateTime::new(odt); let vs: ValueSet = ValueSetDateTime::new(odt);
crate::valueset::scim_json_reflexive(vs.clone(), r#""1970-01-01T19:17:00Z""#); crate::valueset::scim_json_reflexive(&vs, r#""1970-01-01T19:17:00Z""#);
// Test that we can parse json values into a valueset. // Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive::<ValueSetDateTime>(vs, &[]) crate::valueset::scim_json_put_reflexive::<ValueSetDateTime>(&vs, &[])
} }
} }

View file

@ -183,9 +183,6 @@ mod tests {
fn test_scim_hexstring() { fn test_scim_hexstring() {
let vs: ValueSet = let vs: ValueSet =
ValueSetHexString::new("D68475C760A7A0F6A924C28F095573A967F600D6".to_string()); ValueSetHexString::new("D68475C760A7A0F6A924C28F095573A967F600D6".to_string());
crate::valueset::scim_json_reflexive( crate::valueset::scim_json_reflexive(&vs, r#""D68475C760A7A0F6A924C28F095573A967F600D6""#);
vs.clone(),
r#""D68475C760A7A0F6A924C28F095573A967F600D6""#,
);
} }
} }

View file

@ -1,3 +1,5 @@
use std::io::Cursor;
use image::codecs::jpeg::JpegDecoder; use image::codecs::jpeg::JpegDecoder;
use image::ImageDecoder; use image::ImageDecoder;
use sketching::*; use sketching::*;
@ -79,9 +81,9 @@ pub fn has_trailer(contents: &Vec<u8>) -> Result<bool, ImageValidationError> {
pub fn validate_decoding( pub fn validate_decoding(
filename: &str, filename: &str,
contents: &[u8], contents: &[u8],
limits: image::io::Limits, limits: image::Limits,
) -> Result<(), ImageValidationError> { ) -> Result<(), ImageValidationError> {
let mut decoder = match JpegDecoder::new(contents) { let mut decoder = match JpegDecoder::new(Cursor::new(contents)) {
Ok(val) => val, Ok(val) => val,
Err(err) => { Err(err) => {
return Err(ImageValidationError::InvalidImage(format!( return Err(ImageValidationError::InvalidImage(format!(

View file

@ -1,6 +1,7 @@
#![allow(dead_code)] #![allow(dead_code)]
use crate::valueset::ScimResolveStatus; use crate::valueset::ScimResolveStatus;
use std::fmt::Display; use std::fmt::Display;
use std::io::Cursor;
use crate::be::dbvalue::DbValueImage; use crate::be::dbvalue::DbValueImage;
use crate::prelude::*; use crate::prelude::*;
@ -37,8 +38,8 @@ pub trait ImageValueThings {
/// A sha256 of the filename/type/contents /// A sha256 of the filename/type/contents
fn hash_imagevalue(&self) -> String; fn hash_imagevalue(&self) -> String;
fn get_limits(&self) -> image::io::Limits { fn get_limits(&self) -> image::Limits {
let mut limits = image::io::Limits::default(); let mut limits = image::Limits::default();
limits.max_image_height = Some(MAX_IMAGE_HEIGHT); limits.max_image_height = Some(MAX_IMAGE_HEIGHT);
limits.max_image_width = Some(MAX_IMAGE_WIDTH); limits.max_image_width = Some(MAX_IMAGE_WIDTH);
limits limits
@ -148,7 +149,7 @@ impl ImageValueThings for ImageValue {
/// validate the GIF file contents, and that it's actually a GIF /// validate the GIF file contents, and that it's actually a GIF
fn validate_is_gif(&self) -> Result<(), ImageValidationError> { fn validate_is_gif(&self) -> Result<(), ImageValidationError> {
let Ok(mut decoder) = GifDecoder::new(&self.contents[..]) else { let Ok(mut decoder) = GifDecoder::new(Cursor::new(&self.contents[..])) else {
return Err(ImageValidationError::InvalidImage( return Err(ImageValidationError::InvalidImage(
"Failed to parse GIF".to_string(), "Failed to parse GIF".to_string(),
)); ));
@ -189,7 +190,7 @@ impl ImageValueThings for ImageValue {
)); ));
} }
let Ok(mut decoder) = WebPDecoder::new(&self.contents[..]) else { let Ok(mut decoder) = WebPDecoder::new(Cursor::new(&self.contents[..])) else {
return Err(ImageValidationError::InvalidImage( return Err(ImageValidationError::InvalidImage(
"Failed to parse WebP file".to_string(), "Failed to parse WebP file".to_string(),
)); ));
@ -532,7 +533,7 @@ mod tests {
"142dc7984dd548dd5dacfe2ad30f8473e3217e39b3b6c8d17a0cf6e4e24b02e0" "142dc7984dd548dd5dacfe2ad30f8473e3217e39b3b6c8d17a0cf6e4e24b02e0"
]"#; ]"#;
crate::valueset::scim_json_reflexive(vs.clone(), data); crate::valueset::scim_json_reflexive(&vs, data);
} }
*/ */
} }

View file

@ -226,9 +226,9 @@ mod tests {
#[test] #[test]
fn test_scim_iname() { fn test_scim_iname() {
let vs: ValueSet = ValueSetIname::new("stevo"); let vs: ValueSet = ValueSetIname::new("stevo");
crate::valueset::scim_json_reflexive(vs.clone(), r#""stevo""#); crate::valueset::scim_json_reflexive(&vs, r#""stevo""#);
// Test that we can parse json values into a valueset. // Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive::<ValueSetIname>(vs, &[]) crate::valueset::scim_json_put_reflexive::<ValueSetIname>(&vs, &[])
} }
} }

View file

@ -183,9 +183,9 @@ mod tests {
#[test] #[test]
fn test_scim_index() { fn test_scim_index() {
let vs: ValueSet = ValueSetIndex::new(IndexType::Equality); let vs: ValueSet = ValueSetIndex::new(IndexType::Equality);
crate::valueset::scim_json_reflexive(vs.clone(), r#"["EQUALITY"]"#); crate::valueset::scim_json_reflexive(&vs, r#"["EQUALITY"]"#);
// Test that we can parse json values into a valueset. // Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive::<ValueSetIndex>(vs, &[]) crate::valueset::scim_json_put_reflexive::<ValueSetIndex>(&vs, &[])
} }
} }

View file

@ -227,9 +227,9 @@ mod tests {
#[test] #[test]
fn test_scim_iutf8() { fn test_scim_iutf8() {
let vs: ValueSet = ValueSetIutf8::new("lowercase string"); let vs: ValueSet = ValueSetIutf8::new("lowercase string");
crate::valueset::scim_json_reflexive(vs.clone(), r#""lowercase string""#); crate::valueset::scim_json_reflexive(&vs, r#""lowercase string""#);
// Test that we can parse json values into a valueset. // Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive::<ValueSetIutf8>(vs, &[]) crate::valueset::scim_json_put_reflexive::<ValueSetIutf8>(&vs, &[])
} }
} }

View file

@ -205,9 +205,9 @@ mod tests {
"{\"pres\":\"class\"}" "{\"pres\":\"class\"}"
] ]
"#; "#;
crate::valueset::scim_json_reflexive(vs.clone(), data); crate::valueset::scim_json_reflexive(&vs, data);
// Test that we can parse json values into a valueset. // Test that we can parse json values into a valueset.
// crate::valueset::scim_json_put_reflexive::<ValueSetJsonFilter>(vs, &[]) // crate::valueset::scim_json_put_reflexive::<ValueSetJsonFilter>(&vs, &[])
} }
} }

View file

@ -649,6 +649,6 @@ mod tests {
} }
] ]
"#; "#;
crate::valueset::scim_json_reflexive(vs, data); crate::valueset::scim_json_reflexive(&vs, data);
} }
} }

View file

@ -995,7 +995,7 @@ pub fn from_db_valueset_v2(dbvs: DbValueSetV2) -> Result<ValueSet, OperationErro
} }
#[cfg(test)] #[cfg(test)]
pub(crate) fn scim_json_reflexive(vs: ValueSet, data: &str) { pub(crate) fn scim_json_reflexive(vs: &ValueSet, data: &str) {
let scim_value = vs.to_scim_value().unwrap().assume_resolved(); let scim_value = vs.to_scim_value().unwrap().assume_resolved();
let strout = serde_json::to_string_pretty(&scim_value).unwrap(); let strout = serde_json::to_string_pretty(&scim_value).unwrap();
@ -1012,25 +1012,27 @@ pub(crate) fn scim_json_reflexive(vs: ValueSet, data: &str) {
#[cfg(test)] #[cfg(test)]
pub(crate) fn scim_json_reflexive_unresolved( pub(crate) fn scim_json_reflexive_unresolved(
write_txn: &mut QueryServerWriteTransaction, write_txn: &mut QueryServerWriteTransaction,
vs: ValueSet, vs: &ValueSet,
data: &str, data: &str,
) { ) {
let scim_int_value = vs.to_scim_value().unwrap().assume_unresolved(); let scim_int_value = vs.to_scim_value().unwrap().assume_unresolved();
let scim_value = write_txn.resolve_scim_interim(scim_int_value).unwrap(); let scim_value = write_txn.resolve_scim_interim(scim_int_value).unwrap();
let strout = serde_json::to_string_pretty(&scim_value).unwrap(); let strout = serde_json::to_string_pretty(&scim_value).expect("Failed to serialize");
eprintln!("{}", strout); eprintln!("{}", strout);
let json_value: serde_json::Value = serde_json::to_value(&scim_value).unwrap(); let json_value: serde_json::Value =
serde_json::to_value(&scim_value).expect("Failed to convert to JSON");
let expect: serde_json::Value = serde_json::from_str(data).unwrap(); let expect: serde_json::Value =
serde_json::from_str(data).expect("Failed to parse expected JSON");
assert_eq!(json_value, expect); assert_eq!(json_value, expect);
} }
#[cfg(test)] #[cfg(test)]
pub(crate) fn scim_json_put_reflexive<T: ValueSetScimPut>( pub(crate) fn scim_json_put_reflexive<T: ValueSetScimPut>(
expect_vs: ValueSet, expect_vs: &ValueSet,
additional_tests: &[(JsonValue, ValueSet)], additional_tests: &[(JsonValue, ValueSet)],
) { ) {
let scim_value = expect_vs.to_scim_value().unwrap().assume_resolved(); let scim_value = expect_vs.to_scim_value().unwrap().assume_resolved();
@ -1041,7 +1043,7 @@ pub(crate) fn scim_json_put_reflexive<T: ValueSetScimPut>(
let generic = serde_json::to_value(scim_value).unwrap(); let generic = serde_json::to_value(scim_value).unwrap();
// Check that we can turn back into a vs from the generic version. // Check that we can turn back into a vs from the generic version.
let vs = T::from_scim_json_put(generic).unwrap().assume_resolved(); let vs = T::from_scim_json_put(generic).unwrap().assume_resolved();
assert_eq!(&vs, &expect_vs); assert_eq!(&vs, expect_vs);
// For each additional check, assert they work as expected. // For each additional check, assert they work as expected.
for (jv, expect_vs) in additional_tests { for (jv, expect_vs) in additional_tests {
@ -1053,7 +1055,7 @@ pub(crate) fn scim_json_put_reflexive<T: ValueSetScimPut>(
#[cfg(test)] #[cfg(test)]
pub(crate) fn scim_json_put_reflexive_unresolved<T: ValueSetScimPut>( pub(crate) fn scim_json_put_reflexive_unresolved<T: ValueSetScimPut>(
write_txn: &mut QueryServerWriteTransaction, write_txn: &mut QueryServerWriteTransaction,
expect_vs: ValueSet, expect_vs: &ValueSet,
additional_tests: &[(JsonValue, ValueSet)], additional_tests: &[(JsonValue, ValueSet)],
) { ) {
let scim_int_value = expect_vs.to_scim_value().unwrap().assume_unresolved(); let scim_int_value = expect_vs.to_scim_value().unwrap().assume_unresolved();
@ -1063,7 +1065,7 @@ pub(crate) fn scim_json_put_reflexive_unresolved<T: ValueSetScimPut>(
// Check that we can turn back into a vs from the generic version. // Check that we can turn back into a vs from the generic version.
let vs_inter = T::from_scim_json_put(generic).unwrap().assume_unresolved(); let vs_inter = T::from_scim_json_put(generic).unwrap().assume_unresolved();
let vs = write_txn.resolve_valueset_intermediate(vs_inter).unwrap(); let vs = write_txn.resolve_valueset_intermediate(vs_inter).unwrap();
assert_eq!(&vs, &expect_vs); assert_eq!(&vs, expect_vs);
// For each additional check, assert they work as expected. // For each additional check, assert they work as expected.
for (jv, expect_vs) in additional_tests { for (jv, expect_vs) in additional_tests {

View file

@ -189,12 +189,9 @@ mod tests {
fn test_scim_nsuniqueid() { fn test_scim_nsuniqueid() {
let vs: ValueSet = let vs: ValueSet =
ValueSetNsUniqueId::new("3a163ca0-47624620-a18806b7-50c84c86".to_string()); ValueSetNsUniqueId::new("3a163ca0-47624620-a18806b7-50c84c86".to_string());
crate::valueset::scim_json_reflexive( crate::valueset::scim_json_reflexive(&vs, r#""3a163ca0-47624620-a18806b7-50c84c86""#);
vs.clone(),
r#""3a163ca0-47624620-a18806b7-50c84c86""#,
);
// Test that we can parse json values into a valueset. // Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive::<ValueSetNsUniqueId>(vs, &[]) crate::valueset::scim_json_put_reflexive::<ValueSetNsUniqueId>(&vs, &[])
} }
} }

View file

@ -898,10 +898,10 @@ mod tests {
fn test_scim_oauth2_scope() { fn test_scim_oauth2_scope() {
let vs: ValueSet = ValueSetOauthScope::new("fully_sick_scope_m8".to_string()); let vs: ValueSet = ValueSetOauthScope::new("fully_sick_scope_m8".to_string());
let data = r#"["fully_sick_scope_m8"]"#; let data = r#"["fully_sick_scope_m8"]"#;
crate::valueset::scim_json_reflexive(vs.clone(), data); crate::valueset::scim_json_reflexive(&vs, data);
// Test that we can parse json values into a valueset. // Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive::<ValueSetOauthScope>(vs, &[]) crate::valueset::scim_json_put_reflexive::<ValueSetOauthScope>(&vs, &[])
} }
#[qs_test] #[qs_test]
@ -930,12 +930,12 @@ mod tests {
} }
] ]
"#; "#;
crate::valueset::scim_json_reflexive_unresolved(&mut write_txn, vs.clone(), data); crate::valueset::scim_json_reflexive_unresolved(&mut write_txn, &vs, data);
// Test that we can parse json values into a valueset. // Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive_unresolved::<ValueSetOauthScopeMap>( crate::valueset::scim_json_put_reflexive_unresolved::<ValueSetOauthScopeMap>(
&mut write_txn, &mut write_txn,
vs, &vs,
&[], &[],
); );
@ -970,12 +970,12 @@ mod tests {
} }
] ]
"#; "#;
crate::valueset::scim_json_reflexive_unresolved(&mut write_txn, vs.clone(), data); crate::valueset::scim_json_reflexive_unresolved(&mut write_txn, &vs, data);
// Test that we can parse json values into a valueset. // Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive_unresolved::<ValueSetOauthClaimMap>( crate::valueset::scim_json_put_reflexive_unresolved::<ValueSetOauthClaimMap>(
&mut write_txn, &mut write_txn,
vs, &vs,
&[], &[],
); );

View file

@ -205,6 +205,6 @@ mod tests {
#[test] #[test]
fn test_scim_restricted() { fn test_scim_restricted() {
let vs: ValueSet = ValueSetRestricted::new("Test".to_string()); let vs: ValueSet = ValueSetRestricted::new("Test".to_string());
crate::valueset::scim_json_reflexive(vs, r#""Test""#); crate::valueset::scim_json_reflexive(&vs, r#""Test""#);
} }
} }

View file

@ -1920,7 +1920,7 @@ mod tests {
} }
] ]
"#; "#;
crate::valueset::scim_json_reflexive(vs, data); crate::valueset::scim_json_reflexive(&vs, data);
} }
#[test] #[test]
@ -1948,6 +1948,6 @@ mod tests {
] ]
"#; "#;
crate::valueset::scim_json_reflexive(vs, data); crate::valueset::scim_json_reflexive(&vs, data);
} }
} }

View file

@ -188,6 +188,6 @@ mod tests {
#[test] #[test]
fn test_scim_spn() { fn test_scim_spn() {
let vs: ValueSet = ValueSetSpn::new(("claire".to_string(), "example.com".to_string())); let vs: ValueSet = ValueSetSpn::new(("claire".to_string(), "example.com".to_string()));
crate::valueset::scim_json_reflexive(vs, r#""claire@example.com""#); crate::valueset::scim_json_reflexive(&vs, r#""claire@example.com""#);
} }
} }

View file

@ -247,9 +247,9 @@ mod tests {
} }
] ]
"#; "#;
crate::valueset::scim_json_reflexive(vs.clone(), data); crate::valueset::scim_json_reflexive(&vs, data);
// Test that we can parse json values into a valueset. // Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive::<ValueSetSshKey>(vs, &[]) crate::valueset::scim_json_put_reflexive::<ValueSetSshKey>(&vs, &[])
} }
} }

View file

@ -188,9 +188,9 @@ mod tests {
#[test] #[test]
fn test_scim_syntax() { fn test_scim_syntax() {
let vs: ValueSet = ValueSetSyntax::new(SyntaxType::Uuid); let vs: ValueSet = ValueSetSyntax::new(SyntaxType::Uuid);
crate::valueset::scim_json_reflexive(vs.clone(), r#""UUID""#); crate::valueset::scim_json_reflexive(&vs, r#""UUID""#);
// Test that we can parse json values into a valueset. // Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive::<ValueSetSyntax>(vs, &[]) crate::valueset::scim_json_put_reflexive::<ValueSetSyntax>(&vs, &[])
} }
} }

View file

@ -163,9 +163,9 @@ mod tests {
#[test] #[test]
fn test_scim_uihint() { fn test_scim_uihint() {
let vs: ValueSet = ValueSetUiHint::new(UiHint::PosixAccount); let vs: ValueSet = ValueSetUiHint::new(UiHint::PosixAccount);
crate::valueset::scim_json_reflexive(vs.clone(), r#"["posixaccount"]"#); crate::valueset::scim_json_reflexive(&vs, r#"["posixaccount"]"#);
// Test that we can parse json values into a valueset. // Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive::<ValueSetUiHint>(vs, &[]) crate::valueset::scim_json_put_reflexive::<ValueSetUiHint>(&vs, &[])
} }
} }

View file

@ -196,9 +196,9 @@ mod tests {
#[test] #[test]
fn test_scim_uint32() { fn test_scim_uint32() {
let vs: ValueSet = ValueSetUint32::new(69); let vs: ValueSet = ValueSetUint32::new(69);
crate::valueset::scim_json_reflexive(vs.clone(), "69"); crate::valueset::scim_json_reflexive(&vs, "69");
// Test that we can parse json values into a valueset. // Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive::<ValueSetUint32>(vs, &[]) crate::valueset::scim_json_put_reflexive::<ValueSetUint32>(&vs, &[])
} }
} }

View file

@ -183,9 +183,9 @@ mod tests {
fn test_scim_url() { fn test_scim_url() {
let u = Url::parse("https://idm.example.com").unwrap(); let u = Url::parse("https://idm.example.com").unwrap();
let vs: ValueSet = ValueSetUrl::new(u); let vs: ValueSet = ValueSetUrl::new(u);
crate::valueset::scim_json_reflexive(vs.clone(), r#""https://idm.example.com/""#); crate::valueset::scim_json_reflexive(&vs, r#""https://idm.example.com/""#);
// Test that we can parse json values into a valueset. // Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive::<ValueSetUrl>(vs, &[]) crate::valueset::scim_json_put_reflexive::<ValueSetUrl>(&vs, &[])
} }
} }

View file

@ -246,9 +246,9 @@ mod tests {
fn test_scim_utf8() { fn test_scim_utf8() {
let vs: ValueSet = ValueSetUtf8::new("Test".to_string()); let vs: ValueSet = ValueSetUtf8::new("Test".to_string());
// Test that the output json matches some known str // Test that the output json matches some known str
crate::valueset::scim_json_reflexive(vs.clone(), r#""Test""#); crate::valueset::scim_json_reflexive(&vs, r#""Test""#);
// Test that we can parse json values into a valueset. // Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive::<ValueSetUtf8>(vs, &[]) crate::valueset::scim_json_put_reflexive::<ValueSetUtf8>(&vs, &[])
} }
} }

View file

@ -422,10 +422,10 @@ mod tests {
let data = r#""4d21d04a-dc0e-42eb-b850-34dd180b107f""#; let data = r#""4d21d04a-dc0e-42eb-b850-34dd180b107f""#;
crate::valueset::scim_json_reflexive(vs.clone(), data); crate::valueset::scim_json_reflexive(&vs, data);
// Test that we can parse json values into a valueset. // Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive::<ValueSetUuid>(vs, &[]) crate::valueset::scim_json_put_reflexive::<ValueSetUuid>(&vs, &[])
} }
#[qs_test] #[qs_test]
@ -449,12 +449,12 @@ mod tests {
let data = r#"[{"uuid": "4d21d04a-dc0e-42eb-b850-34dd180b107f", "value": "testperson1@example.com"}]"#; let data = r#"[{"uuid": "4d21d04a-dc0e-42eb-b850-34dd180b107f", "value": "testperson1@example.com"}]"#;
crate::valueset::scim_json_reflexive_unresolved(&mut write_txn, vs.clone(), data); crate::valueset::scim_json_reflexive_unresolved(&mut write_txn, &vs, data);
// Test that we can parse json values into a valueset. // Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive_unresolved::<ValueSetRefer>( crate::valueset::scim_json_put_reflexive_unresolved::<ValueSetRefer>(
&mut write_txn, &mut write_txn,
vs, &vs,
&[], &[],
); );

View file

@ -10,16 +10,17 @@ const ALLOWED_ATTRIBUTES: &[&str] = &[
"threads", "threads",
"db_path", "db_path",
"maximum_request", "maximum_request",
"trust_x_forward_for", "http_client_address_info",
"role", "role",
"output_mode", "output_mode",
"log_level", "log_level",
"ldap", "ldap",
"with_test_env",
]; ];
#[derive(Default)] #[derive(Default)]
struct Flags { struct Flags {
ldap: bool, target_wants_test_env: bool,
} }
fn parse_attributes( fn parse_attributes(
@ -60,8 +61,11 @@ fn parse_attributes(
.unwrap_or_default() .unwrap_or_default()
.as_str() .as_str()
{ {
"with_test_env" => {
flags.target_wants_test_env = true;
}
"ldap" => { "ldap" => {
flags.ldap = true; flags.target_wants_test_env = true;
field_modifications.extend(quote! { field_modifications.extend(quote! {
ldapbindaddress: Some("on".to_string()),}) ldapbindaddress: Some("on".to_string()),})
} }
@ -134,7 +138,7 @@ pub(crate) fn test(args: TokenStream, item: TokenStream) -> TokenStream {
#[::core::prelude::v1::test] #[::core::prelude::v1::test]
}; };
let test_fn_args = if flags.ldap { let test_fn_args = if flags.target_wants_test_env {
quote! { quote! {
&test_env &test_env
} }

View file

@ -44,7 +44,7 @@ pub fn cli_kanidm(_input: TokenStream) -> TokenStream {
.run() .run()
.unwrap(); .unwrap();
let mut kanidm = kanidm.command(); let mut kanidm = kanidm.command();
kanidm.env("KANIDM_URL", &rsclient.get_url().to_string()); kanidm.env("KANIDM_URL", rsclient.get_url().to_string());
kanidm.env("KANIDM_TOKEN_CACHE_PATH", &token_cache_path); kanidm.env("KANIDM_TOKEN_CACHE_PATH", &token_cache_path);
kanidm kanidm
} }

View file

@ -53,6 +53,10 @@ escargot = "0.5.13"
# used for webdriver testing # used for webdriver testing
fantoccini = { version = "0.21.5" } fantoccini = { version = "0.21.5" }
futures = { workspace = true } futures = { workspace = true }
hex = { workspace = true }
hyper = { workspace = true }
http-body-util = { workspace = true }
hyper-util = { workspace = true }
ldap3_client = { workspace = true } ldap3_client = { workspace = true }
oauth2_ext = { workspace = true, default-features = false, features = [ oauth2_ext = { workspace = true, default-features = false, features = [
"reqwest", "reqwest",

View file

@ -15,7 +15,7 @@ use kanidm_proto::internal::{Filter, Modify, ModifyList};
use kanidmd_core::config::{Configuration, IntegrationTestConfig}; use kanidmd_core::config::{Configuration, IntegrationTestConfig};
use kanidmd_core::{create_server_core, CoreHandle}; use kanidmd_core::{create_server_core, CoreHandle};
use kanidmd_lib::prelude::{Attribute, NAME_SYSTEM_ADMINS}; use kanidmd_lib::prelude::{Attribute, NAME_SYSTEM_ADMINS};
use std::net::TcpStream; use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpStream};
use std::sync::atomic::{AtomicU16, Ordering}; use std::sync::atomic::{AtomicU16, Ordering};
use tokio::task; use tokio::task;
use tracing::error; use tracing::error;
@ -64,6 +64,7 @@ fn port_loop() -> u16 {
pub struct AsyncTestEnvironment { pub struct AsyncTestEnvironment {
pub rsclient: KanidmClient, pub rsclient: KanidmClient,
pub http_sock_addr: SocketAddr,
pub core_handle: CoreHandle, pub core_handle: CoreHandle,
pub ldap_url: Option<Url>, pub ldap_url: Option<Url>,
} }
@ -86,8 +87,9 @@ pub async fn setup_async_test(mut config: Configuration) -> AsyncTestEnvironment
let ldap_url = if config.ldapbindaddress.is_some() { let ldap_url = if config.ldapbindaddress.is_some() {
let ldapport = port_loop(); let ldapport = port_loop();
config.ldapbindaddress = Some(format!("127.0.0.1:{}", ldapport)); let ldap_sock_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), ldapport);
Url::parse(&format!("ldap://127.0.0.1:{}", ldapport)) config.ldapbindaddress = Some(ldap_sock_addr.to_string());
Url::parse(&format!("ldap://{}", ldap_sock_addr))
.inspect_err(|err| error!(?err, "ldap address setup")) .inspect_err(|err| error!(?err, "ldap address setup"))
.ok() .ok()
} else { } else {
@ -95,7 +97,9 @@ pub async fn setup_async_test(mut config: Configuration) -> AsyncTestEnvironment
}; };
// Setup the address and origin.. // Setup the address and origin..
config.address = format!("127.0.0.1:{}", port); let http_sock_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), port);
config.address = http_sock_addr.to_string();
config.integration_test_config = Some(int_config); config.integration_test_config = Some(int_config);
config.domain = "localhost".to_string(); config.domain = "localhost".to_string();
config.origin.clone_from(&addr); config.origin.clone_from(&addr);
@ -123,6 +127,7 @@ pub async fn setup_async_test(mut config: Configuration) -> AsyncTestEnvironment
AsyncTestEnvironment { AsyncTestEnvironment {
rsclient, rsclient,
http_sock_addr,
core_handle, core_handle,
ldap_url, ldap_url,
} }

View file

@ -10,7 +10,7 @@ async fn test_v1_group_id_patch(rsclient: &KanidmClient) {
.await; .await;
assert!(res.is_ok()); assert!(res.is_ok());
create_user(&rsclient, "foo", "foogroup").await; create_user(rsclient, "foo", "foogroup").await;
let post_body = serde_json::json!({"attrs": { ATTR_DESCRIPTION : ["Fancy group change"]}}); let post_body = serde_json::json!({"attrs": { ATTR_DESCRIPTION : ["Fancy group change"]}});
@ -31,7 +31,7 @@ async fn test_v1_group_id_attr_post(rsclient: &KanidmClient) {
.await; .await;
assert!(res.is_ok()); assert!(res.is_ok());
create_user(&rsclient, "foo", "foogroup").await; create_user(rsclient, "foo", "foogroup").await;
let post_body = serde_json::json!(["foo"]); let post_body = serde_json::json!(["foo"]);

View file

@ -1,193 +0,0 @@
use std::{
net::{IpAddr, Ipv4Addr},
str::FromStr,
};
use kanidm_client::KanidmClient;
use kanidm_proto::constants::X_FORWARDED_FOR;
const DEFAULT_IP_ADDRESS: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
// *test where we don't trust the x-forwarded-for header
#[kanidmd_testkit::test(trust_x_forward_for = false)]
async fn dont_trust_xff_send_header(rsclient: &KanidmClient) {
let client = rsclient.client();
let res = client
.get(rsclient.make_url("/v1/debug/ipinfo"))
.header(
X_FORWARDED_FOR,
"An invalid header that will get through!!!",
)
.send()
.await
.unwrap();
let ip_res: IpAddr = res
.json()
.await
.expect("Failed to parse response as IpAddr");
assert_eq!(ip_res, DEFAULT_IP_ADDRESS);
}
#[kanidmd_testkit::test(trust_x_forward_for = false)]
async fn dont_trust_xff_dont_send_header(rsclient: &KanidmClient) {
let client = rsclient.client();
let res = client
.get(rsclient.make_url("/v1/debug/ipinfo"))
.header(
X_FORWARDED_FOR,
"An invalid header that will get through!!!",
)
.send()
.await
.unwrap();
let body = res.bytes().await.unwrap();
let ip_res: IpAddr = serde_json::from_slice(&body).unwrap_or_else(|op| {
panic!(
"Failed to parse response as IpAddr: {:?} body: {:?}",
op, body,
)
});
eprintln!("Body: {:?}", body);
assert_eq!(ip_res, DEFAULT_IP_ADDRESS);
}
// *test where we trust the x-forwarded-for header
#[kanidmd_testkit::test(trust_x_forward_for = true)]
async fn trust_xff_send_invalid_header_single_value(rsclient: &KanidmClient) {
let client = rsclient.client();
let res = client
.get(rsclient.make_url("/v1/debug/ipinfo"))
.header(
X_FORWARDED_FOR,
"An invalid header that will get through!!!",
)
.send()
.await
.unwrap();
assert_eq!(res.status(), 400);
}
// TODO: Right now we reject the request only if the leftmost address is invalid. In the future that could change so we could also have a test
// with a valid leftmost address and an invalid address later in the list. Right now it wouldn't work.
//
#[kanidmd_testkit::test(trust_x_forward_for = true)]
async fn trust_xff_send_invalid_header_multiple_values(rsclient: &KanidmClient) {
let client = rsclient.client();
let res = client
.get(rsclient.make_url("/v1/debug/ipinfo"))
.header(
X_FORWARDED_FOR,
"203.0.113.195_noooo_my_ip_address, 2001:db8:85a3:8d3:1319:8a2e:370:7348",
)
.send()
.await
.unwrap();
assert_eq!(res.status(), 400);
}
#[kanidmd_testkit::test(trust_x_forward_for = true)]
async fn trust_xff_send_valid_header_single_ipv4_address(rsclient: &KanidmClient) {
let ip_addr = "2001:db8:85a3:8d3:1319:8a2e:370:7348";
let client = rsclient.client();
let res = client
.get(rsclient.make_url("/v1/debug/ipinfo"))
.header(X_FORWARDED_FOR, ip_addr)
.send()
.await
.unwrap();
let ip_res: IpAddr = res
.json()
.await
.expect("Failed to parse response as Vec<IpAddr>");
assert_eq!(ip_res, IpAddr::from_str(ip_addr).unwrap());
}
#[kanidmd_testkit::test(trust_x_forward_for = true)]
async fn trust_xff_send_valid_header_single_ipv6_address(rsclient: &KanidmClient) {
let ip_addr = "203.0.113.195";
let client = rsclient.client();
let res = client
.get(rsclient.make_url("/v1/debug/ipinfo"))
.header(X_FORWARDED_FOR, ip_addr)
.send()
.await
.unwrap();
let ip_res: IpAddr = res
.json()
.await
.expect("Failed to parse response as Vec<IpAddr>");
assert_eq!(ip_res, IpAddr::from_str(ip_addr).unwrap());
}
#[kanidmd_testkit::test(trust_x_forward_for = true)]
async fn trust_xff_send_valid_header_multiple_address(rsclient: &KanidmClient) {
let first_ip_addr = "203.0.113.195, 2001:db8:85a3:8d3:1319:8a2e:370:7348";
let client = rsclient.client();
let res = client
.get(rsclient.make_url("/v1/debug/ipinfo"))
.header(X_FORWARDED_FOR, first_ip_addr)
.send()
.await
.unwrap();
let ip_res: IpAddr = res
.json()
.await
.expect("Failed to parse response as Vec<IpAddr>");
assert_eq!(
ip_res,
IpAddr::from_str(first_ip_addr.split(",").collect::<Vec<&str>>()[0]).unwrap()
);
let second_ip_addr = "2001:db8:85a3:8d3:1319:8a2e:370:7348, 198.51.100.178, 203.0.113.195";
let res = client
.get(rsclient.make_url("/v1/debug/ipinfo"))
.header(X_FORWARDED_FOR, second_ip_addr)
.send()
.await
.unwrap();
let ip_res: IpAddr = res
.json()
.await
.expect("Failed to parse response as Vec<IpAddr>");
assert_eq!(
ip_res,
IpAddr::from_str(second_ip_addr.split(",").collect::<Vec<&str>>()[0]).unwrap()
);
}
#[kanidmd_testkit::test(trust_x_forward_for = true)]
async fn trust_xff_dont_send_header(rsclient: &KanidmClient) {
let client = rsclient.client();
let res = client
.get(rsclient.make_url("/v1/debug/ipinfo"))
.send()
.await
.unwrap();
let ip_res: IpAddr = res
.json()
.await
.expect("Failed to parse response as Vec<IpAddr>");
assert_eq!(ip_res, DEFAULT_IP_ADDRESS);
}

View file

@ -17,8 +17,8 @@ static USER_B_NAME: &str = "valid_user_b";
#[kanidmd_testkit::test] #[kanidmd_testkit::test]
async fn test_not_authenticated(rsclient: &KanidmClient) { async fn test_not_authenticated(rsclient: &KanidmClient) {
// basically here we try a bit of all the possible combinations while unauthenticated to check it's not working // basically here we try a bit of all the possible combinations while unauthenticated to check it's not working
setup_server(&rsclient).await; setup_server(rsclient).await;
create_user(&rsclient, USER_A_NAME).await; create_user(rsclient, USER_A_NAME).await;
let _ = rsclient.logout().await; let _ = rsclient.logout().await;
let res = rsclient let res = rsclient
.idm_person_identify_user(USER_A_NAME, IdentifyUserRequest::Start) .idm_person_identify_user(USER_A_NAME, IdentifyUserRequest::Start)
@ -47,11 +47,11 @@ async fn test_not_authenticated(rsclient: &KanidmClient) {
#[kanidmd_testkit::test] #[kanidmd_testkit::test]
async fn test_non_existing_user_id(rsclient: &KanidmClient) { async fn test_non_existing_user_id(rsclient: &KanidmClient) {
setup_server(&rsclient).await; setup_server(rsclient).await;
create_user(&rsclient, USER_A_NAME).await; create_user(rsclient, USER_A_NAME).await;
create_user(&rsclient, USER_B_NAME).await; create_user(rsclient, USER_B_NAME).await;
let non_existing_user = "non_existing_user"; let non_existing_user = "non_existing_user";
login_with_user(&rsclient, USER_A_NAME).await; login_with_user(rsclient, USER_A_NAME).await;
let res: Result<IdentifyUserResponse, kanidm_client::ClientError> = rsclient let res: Result<IdentifyUserResponse, kanidm_client::ClientError> = rsclient
.idm_person_identify_user(non_existing_user, IdentifyUserRequest::Start) .idm_person_identify_user(non_existing_user, IdentifyUserRequest::Start)
.await; .await;
@ -87,9 +87,9 @@ async fn test_non_existing_user_id(rsclient: &KanidmClient) {
// Each tests is named like `test_{api input}_response_{expected api output}_or_{expected api output}` // Each tests is named like `test_{api input}_response_{expected api output}_or_{expected api output}`
#[kanidmd_testkit::test] #[kanidmd_testkit::test]
async fn test_start_response_identity_verification_available(rsclient: &KanidmClient) { async fn test_start_response_identity_verification_available(rsclient: &KanidmClient) {
setup_server(&rsclient).await; setup_server(rsclient).await;
create_user(&rsclient, USER_A_NAME).await; create_user(rsclient, USER_A_NAME).await;
login_with_user(&rsclient, USER_A_NAME).await; login_with_user(rsclient, USER_A_NAME).await;
let response = rsclient let response = rsclient
.idm_person_identify_user(USER_A_NAME, IdentifyUserRequest::Start) .idm_person_identify_user(USER_A_NAME, IdentifyUserRequest::Start)
@ -106,10 +106,10 @@ async fn test_start_response_identity_verification_available(rsclient: &KanidmCl
// `Start`, that is WaitForCode or ProvideCode // `Start`, that is WaitForCode or ProvideCode
#[kanidmd_testkit::test] #[kanidmd_testkit::test]
async fn test_start_response_wait_for_code_or_provide_code(rsclient: &KanidmClient) { async fn test_start_response_wait_for_code_or_provide_code(rsclient: &KanidmClient) {
setup_server(&rsclient).await; setup_server(rsclient).await;
let user_a_uuid = create_user(&rsclient, USER_A_NAME).await; let user_a_uuid = create_user(rsclient, USER_A_NAME).await;
let user_b_uuid = create_user(&rsclient, USER_B_NAME).await; let user_b_uuid = create_user(rsclient, USER_B_NAME).await;
login_with_user(&rsclient, USER_A_NAME).await; login_with_user(rsclient, USER_A_NAME).await;
let response = rsclient let response = rsclient
.idm_person_identify_user(USER_B_NAME, IdentifyUserRequest::Start) .idm_person_identify_user(USER_B_NAME, IdentifyUserRequest::Start)
.await; .await;
@ -130,10 +130,10 @@ async fn test_start_response_wait_for_code_or_provide_code(rsclient: &KanidmClie
#[kanidmd_testkit::test] #[kanidmd_testkit::test]
async fn test_provide_code_response_code_failure_or_provide_code(rsclient: &KanidmClient) { async fn test_provide_code_response_code_failure_or_provide_code(rsclient: &KanidmClient) {
setup_server(&rsclient).await; setup_server(rsclient).await;
let user_a_uuid = create_user(&rsclient, USER_A_NAME).await; let user_a_uuid = create_user(rsclient, USER_A_NAME).await;
let user_b_uuid = create_user(&rsclient, USER_B_NAME).await; let user_b_uuid = create_user(rsclient, USER_B_NAME).await;
login_with_user(&rsclient, USER_A_NAME).await; login_with_user(rsclient, USER_A_NAME).await;
let response = rsclient let response = rsclient
.idm_person_identify_user( .idm_person_identify_user(
USER_B_NAME, USER_B_NAME,
@ -158,12 +158,12 @@ async fn test_provide_code_response_code_failure_or_provide_code(rsclient: &Kani
// here we actually test the full idm flow by duplicating the server // here we actually test the full idm flow by duplicating the server
#[kanidmd_testkit::test] #[kanidmd_testkit::test]
async fn test_full_identification_flow(rsclient: &KanidmClient) { async fn test_full_identification_flow(rsclient: &KanidmClient) {
setup_server(&rsclient).await; setup_server(rsclient).await;
let user_a_uuid = create_user(&rsclient, USER_A_NAME).await; let user_a_uuid = create_user(rsclient, USER_A_NAME).await;
let user_b_uuid = create_user(&rsclient, USER_B_NAME).await; let user_b_uuid = create_user(rsclient, USER_B_NAME).await;
//user A session //user A session
let valid_user_a_client = rsclient; let valid_user_a_client = rsclient;
login_with_user(&valid_user_a_client, USER_A_NAME).await; login_with_user(valid_user_a_client, USER_A_NAME).await;
//user B session //user B session
let valid_user_b_client = valid_user_a_client.new_session().unwrap(); let valid_user_b_client = valid_user_a_client.new_session().unwrap();
login_with_user(&valid_user_b_client, USER_B_NAME).await; login_with_user(&valid_user_b_client, USER_B_NAME).await;

View file

@ -76,10 +76,10 @@ async fn test_webdriver_user_login(rsclient: &KanidmClient) {
use fantoccini::Locator; use fantoccini::Locator;
use kanidmd_testkit::*; use kanidmd_testkit::*;
use std::time::Duration; use std::time::Duration;
login_put_admin_idm_admins(&rsclient).await; login_put_admin_idm_admins(rsclient).await;
create_user_with_all_attrs( create_user_with_all_attrs(
&rsclient, rsclient,
NOT_ADMIN_TEST_USERNAME, NOT_ADMIN_TEST_USERNAME,
Some(NOT_ADMIN_TEST_PASSWORD), Some(NOT_ADMIN_TEST_PASSWORD),
) )
@ -89,7 +89,7 @@ async fn test_webdriver_user_login(rsclient: &KanidmClient) {
handle_error!( handle_error!(
c, c,
c.goto(&rsclient.get_url().to_string()).await, c.goto(rsclient.get_url().to_string()).await,
"Couldn't get URL" "Couldn't get URL"
); );
@ -207,7 +207,7 @@ async fn test_webdriver_user_login(rsclient: &KanidmClient) {
#[kanidmd_testkit::test] #[kanidmd_testkit::test]
async fn test_domain_reset_token_key(rsclient: &KanidmClient) { async fn test_domain_reset_token_key(rsclient: &KanidmClient) {
login_put_admin_idm_admins(&rsclient).await; login_put_admin_idm_admins(rsclient).await;
let token = rsclient.get_token().await.expect("No bearer token present"); let token = rsclient.get_token().await.expect("No bearer token present");
@ -220,7 +220,7 @@ async fn test_domain_reset_token_key(rsclient: &KanidmClient) {
#[kanidmd_testkit::test] #[kanidmd_testkit::test]
async fn test_idm_domain_set_ldap_basedn(rsclient: &KanidmClient) { async fn test_idm_domain_set_ldap_basedn(rsclient: &KanidmClient) {
login_put_admin_idm_admins(&rsclient).await; login_put_admin_idm_admins(rsclient).await;
assert!(rsclient assert!(rsclient
.idm_domain_set_ldap_basedn("dc=krabsarekool,dc=example,dc=com") .idm_domain_set_ldap_basedn("dc=krabsarekool,dc=example,dc=com")
.await .await
@ -233,7 +233,7 @@ async fn test_idm_domain_set_ldap_basedn(rsclient: &KanidmClient) {
#[kanidmd_testkit::test] #[kanidmd_testkit::test]
async fn test_idm_domain_set_ldap_max_queryable_attrs(rsclient: &KanidmClient) { async fn test_idm_domain_set_ldap_max_queryable_attrs(rsclient: &KanidmClient) {
login_put_admin_idm_admins(&rsclient).await; login_put_admin_idm_admins(rsclient).await;
assert!(rsclient assert!(rsclient
.idm_domain_set_ldap_max_queryable_attrs(20) .idm_domain_set_ldap_max_queryable_attrs(20)
.await .await
@ -247,7 +247,7 @@ async fn test_idm_domain_set_ldap_max_queryable_attrs(rsclient: &KanidmClient) {
#[kanidmd_testkit::test] #[kanidmd_testkit::test]
/// Checks that a built-in group idm_all_persons has the "builtin" class as expected. /// Checks that a built-in group idm_all_persons has the "builtin" class as expected.
async fn test_all_persons_has_builtin_class(rsclient: &KanidmClient) { async fn test_all_persons_has_builtin_class(rsclient: &KanidmClient) {
login_put_admin_idm_admins(&rsclient).await; login_put_admin_idm_admins(rsclient).await;
let res = rsclient let res = rsclient
.idm_group_get("idm_all_persons") .idm_group_get("idm_all_persons")
.await .await
@ -306,7 +306,7 @@ async fn test_all_persons_has_builtin_class(rsclient: &KanidmClient) {
/// Testing the CLI doing things. /// Testing the CLI doing things.
async fn test_integration_with_assert_cmd(rsclient: KanidmClient) { async fn test_integration_with_assert_cmd(rsclient: KanidmClient) {
// setup the admin things // setup the admin things
login_put_admin_idm_admins(&rsclient).await; login_put_admin_idm_admins(rsclient).await;
rsclient rsclient
.idm_person_account_primary_credential_set_password( .idm_person_account_primary_credential_set_password(
@ -334,13 +334,13 @@ async fn test_integration_with_assert_cmd(rsclient: KanidmClient) {
assert!(anon_whoami.status.success()); assert!(anon_whoami.status.success());
println!("Output: {:?}", anon_whoami); println!("Output: {:?}", anon_whoami);
test_cmd_admin(&token_cache_path, &rsclient, "login -D admin"); test_cmd_admin(&token_cache_path, rsclient, "login -D admin");
// login as idm_admin // login as idm_admin
test_cmd_idm_admin(&token_cache_path, &rsclient, "login -D idm_admin"); test_cmd_idm_admin(&token_cache_path, rsclient, "login -D idm_admin");
test_cmd_admin_split( test_cmd_admin_split(
&token_cache_path, &token_cache_path,
&rsclient, rsclient,
&[ &[
"service-account", "service-account",
"create", "create",
@ -355,13 +355,13 @@ async fn test_integration_with_assert_cmd(rsclient: KanidmClient) {
test_cmd_admin( test_cmd_admin(
&token_cache_path, &token_cache_path,
&rsclient, rsclient,
&format!("service-account get -D admin {}", NOT_ADMIN_TEST_USERNAME), &format!("service-account get -D admin {}", NOT_ADMIN_TEST_USERNAME),
); );
// updating the display name // updating the display name
test_cmd_admin( test_cmd_admin(
&token_cache_path, &token_cache_path,
&rsclient, rsclient,
&format!( &format!(
"service-account update -D admin {} --displayname cheeseballs", "service-account update -D admin {} --displayname cheeseballs",
NOT_ADMIN_TEST_USERNAME NOT_ADMIN_TEST_USERNAME
@ -370,7 +370,7 @@ async fn test_integration_with_assert_cmd(rsclient: KanidmClient) {
// updating the email // updating the email
test_cmd_admin( test_cmd_admin(
&token_cache_path, &token_cache_path,
&rsclient, rsclient,
&format!( &format!(
"service-account update -D admin {} --mail foo@bar.com", "service-account update -D admin {} --mail foo@bar.com",
NOT_ADMIN_TEST_USERNAME NOT_ADMIN_TEST_USERNAME
@ -380,7 +380,7 @@ async fn test_integration_with_assert_cmd(rsclient: KanidmClient) {
// checking the email was changed // checking the email was changed
let sad = test_cmd_admin( let sad = test_cmd_admin(
&token_cache_path, &token_cache_path,
&rsclient, rsclient,
&format!( &format!(
"service-account get -D admin -o json {}", "service-account get -D admin -o json {}",
NOT_ADMIN_TEST_USERNAME NOT_ADMIN_TEST_USERNAME

View file

@ -0,0 +1,324 @@
use kanidm_client::KanidmClient;
use kanidm_proto::constants::X_FORWARDED_FOR;
use kanidmd_core::config::HttpAddressInfo;
use kanidmd_testkit::AsyncTestEnvironment;
use std::{
net::{IpAddr, Ipv4Addr, SocketAddr},
str::FromStr,
};
use tracing::error;
const DEFAULT_IP_ADDRESS: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
// =====================================================
// *test where we don't trust the x-forwarded-for header
#[kanidmd_testkit::test(http_client_address_info = HttpAddressInfo::None)]
async fn dont_trust_xff_send_header(rsclient: &KanidmClient) {
let client = rsclient.client();
// Send an invalid header to x forwdr for
let res = client
.get(rsclient.make_url("/v1/debug/ipinfo"))
.header(X_FORWARDED_FOR, "a.b.c.d")
.send()
.await
.unwrap();
let ip_res: IpAddr = res
.json()
.await
.expect("Failed to parse response as IpAddr");
assert_eq!(ip_res, DEFAULT_IP_ADDRESS);
// Send a valid header for xforward for, but we don't trust it.
let res = client
.get(rsclient.make_url("/v1/debug/ipinfo"))
.header(X_FORWARDED_FOR, "203.0.113.195")
.send()
.await
.unwrap();
let ip_res: IpAddr = res
.json()
.await
.expect("Failed to parse response as IpAddr");
assert_eq!(ip_res, DEFAULT_IP_ADDRESS);
}
// =====================================================
// *test where we do trust the x-forwarded-for header
#[kanidmd_testkit::test(http_client_address_info = HttpAddressInfo::XForwardFor ( [DEFAULT_IP_ADDRESS].into() ))]
async fn trust_xff_address_set(rsclient: &KanidmClient) {
inner_test_trust_xff(rsclient).await;
}
#[kanidmd_testkit::test(http_client_address_info = HttpAddressInfo::XForwardForAllSourcesTrusted)]
async fn trust_xff_all_addresses_trusted(rsclient: &KanidmClient) {
inner_test_trust_xff(rsclient).await;
}
async fn inner_test_trust_xff(rsclient: &KanidmClient) {
let client = rsclient.client();
// An invalid address.
let res = client
.get(rsclient.make_url("/v1/debug/ipinfo"))
.header(X_FORWARDED_FOR, "a.b.c.d")
.send()
.await
.unwrap();
// Header was invalid
assert_eq!(res.status(), 400);
// An invalid address - what follows doesn't matter, even if it was valid. We only
// care about the left most address anyway.
let res = client
.get(rsclient.make_url("/v1/debug/ipinfo"))
.header(
X_FORWARDED_FOR,
"203.0.113.195_noooo_my_ip_address, 2001:db8:85a3:8d3:1319:8a2e:370:7348",
)
.send()
.await
.unwrap();
assert_eq!(res.status(), 400);
// A valid ipv6 address was provided.
let ip_addr = "2001:db8:85a3:8d3:1319:8a2e:370:7348";
let res = client
.get(rsclient.make_url("/v1/debug/ipinfo"))
.header(X_FORWARDED_FOR, ip_addr)
.send()
.await
.unwrap();
let ip_res: IpAddr = res
.json()
.await
.expect("Failed to parse response as Vec<IpAddr>");
assert_eq!(ip_res, IpAddr::from_str(ip_addr).unwrap());
// A valid ipv4 address was provided.
let ip_addr = "203.0.113.195";
let client = rsclient.client();
let res = client
.get(rsclient.make_url("/v1/debug/ipinfo"))
.header(X_FORWARDED_FOR, ip_addr)
.send()
.await
.unwrap();
let ip_res: IpAddr = res
.json()
.await
.expect("Failed to parse response as Vec<IpAddr>");
assert_eq!(ip_res, IpAddr::from_str(ip_addr).unwrap());
// A valid ipv4 address in the leftmost field.
let first_ip_addr = "203.0.113.195, 2001:db8:85a3:8d3:1319:8a2e:370:7348";
let res = client
.get(rsclient.make_url("/v1/debug/ipinfo"))
.header(X_FORWARDED_FOR, first_ip_addr)
.send()
.await
.unwrap();
let ip_res: IpAddr = res
.json()
.await
.expect("Failed to parse response as Vec<IpAddr>");
assert_eq!(
ip_res,
IpAddr::from_str(first_ip_addr.split(",").collect::<Vec<&str>>()[0]).unwrap()
);
// A valid ipv6 address in the left most field.
let second_ip_addr = "2001:db8:85a3:8d3:1319:8a2e:370:7348, 198.51.100.178, 203.0.113.195";
let res = client
.get(rsclient.make_url("/v1/debug/ipinfo"))
.header(X_FORWARDED_FOR, second_ip_addr)
.send()
.await
.unwrap();
let ip_res: IpAddr = res
.json()
.await
.expect("Failed to parse response as Vec<IpAddr>");
assert_eq!(
ip_res,
IpAddr::from_str(second_ip_addr.split(",").collect::<Vec<&str>>()[0]).unwrap()
);
// If no header is sent, then the connection IP is used.
let res = client
.get(rsclient.make_url("/v1/debug/ipinfo"))
.send()
.await
.unwrap();
let ip_res: IpAddr = res
.json()
.await
.expect("Failed to parse response as Vec<IpAddr>");
assert_eq!(ip_res, DEFAULT_IP_ADDRESS);
}
// =====================================================
// *test where we do trust the PROXY protocol header
//
// NOTE: This is MUCH HARDER TO TEST because we can't just stuff this address
// in front of a reqwest call. We have to open raw connections and write the
// requests to them.
//
// As a result, we are pretty much forced to manually dump binary headers and then
// manually craft get reqs, followed by parsing them.
#[derive(Debug, PartialEq)]
enum ProxyV2Error {
TcpStream,
TcpWrite,
TornWrite,
HttpHandshake,
HttpRequestBuild,
HttpRequest,
HttpBadRequest,
}
async fn proxy_v2_make_request(
http_sock_addr: SocketAddr,
hdr: &[u8],
) -> Result<IpAddr, ProxyV2Error> {
use http_body_util::BodyExt;
use http_body_util::Empty;
use hyper::body::Bytes;
use hyper::Request;
use hyper_util::rt::TokioIo;
use tokio::io::AsyncWriteExt as _;
use tokio::net::TcpStream;
let url = format!("http://{}/v1/debug/ipinfo", http_sock_addr)
.as_str()
.parse::<hyper::Uri>()
.unwrap();
let mut stream = TcpStream::connect(http_sock_addr).await.map_err(|err| {
error!(?err);
ProxyV2Error::TcpStream
})?;
// Write the proxyv2 header
let nbytes = stream.write(hdr).await.map_err(|err| {
error!(?err);
ProxyV2Error::TcpWrite
})?;
if nbytes != hdr.len() {
return Err(ProxyV2Error::TornWrite);
}
let io = TokioIo::new(stream);
let (mut sender, conn) = hyper::client::conn::http1::handshake(io)
.await
.map_err(|err| {
error!(?err);
ProxyV2Error::HttpHandshake
})?;
// Spawn a task to poll the connection, driving the HTTP state
tokio::task::spawn(async move {
if let Err(err) = conn.await {
println!("Connection failed: {:?}", err);
}
});
let authority = url.authority().unwrap().clone();
// Create an HTTP request with an empty body and a HOST header
let req = Request::builder()
.uri(url)
.header(hyper::header::HOST, authority.as_str())
.body(Empty::<Bytes>::new())
.map_err(|err| {
error!(?err);
ProxyV2Error::HttpRequestBuild
})?;
// Await the response...
let mut res = sender.send_request(req).await.map_err(|err| {
error!(?err);
ProxyV2Error::HttpRequest
})?;
println!("Response status: {}", res.status());
if res.status() != 200 {
return Err(ProxyV2Error::HttpBadRequest);
}
let mut data: Vec<u8> = Vec::new();
while let Some(next) = res.frame().await {
let frame = next.unwrap();
if let Some(chunk) = frame.data_ref() {
data.write_all(chunk).await.unwrap();
}
}
tracing::info!(?data);
let ip_res: IpAddr = serde_json::from_slice(&data).unwrap();
tracing::info!(?ip_res);
Ok(ip_res)
}
#[kanidmd_testkit::test(with_test_env = true, http_client_address_info = HttpAddressInfo::ProxyV2 ( [DEFAULT_IP_ADDRESS].into() ))]
async fn trust_proxy_v2_address_set(test_env: &AsyncTestEnvironment) {
// Send with no header - with proxy v2, a header is ALWAYS required
let proxy_hdr: [u8; 0] = [];
let res = proxy_v2_make_request(test_env.http_sock_addr, &proxy_hdr)
.await
.unwrap_err();
// Can't send http request because proxy wasn't sent.
assert_eq!(res, ProxyV2Error::HttpRequest);
// Send with a valid header
let proxy_hdr =
hex::decode("0d0a0d0a000d0a515549540a2111000cac180c76ac180b8fcdcb027d").unwrap();
let res = proxy_v2_make_request(test_env.http_sock_addr, &proxy_hdr)
.await
.unwrap();
// The header was valid
assert_eq!(res, IpAddr::V4(Ipv4Addr::new(172, 24, 12, 118)));
}
#[kanidmd_testkit::test(with_test_env = true, http_client_address_info = HttpAddressInfo::ProxyV2 ( [ IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)) ].into() ))]
async fn trust_proxy_v2_untrusted(test_env: &AsyncTestEnvironment) {
// Send with a valid header, but we aren't a trusted source.
let proxy_hdr =
hex::decode("0d0a0d0a000d0a515549540a2111000cac180c76ac180b8fcdcb027d").unwrap();
let res = proxy_v2_make_request(test_env.http_sock_addr, &proxy_hdr)
.await
.unwrap_err();
// Can't send http request because we aren't trusted to send it, so this
// ends up falling into a http request that is REJECTED.
assert_eq!(res, ProxyV2Error::HttpBadRequest);
}

View file

@ -2,10 +2,10 @@ mod apidocs;
mod domain; mod domain;
mod group; mod group;
mod http_manifest; mod http_manifest;
mod https_extractors;
mod https_middleware; mod https_middleware;
mod identity_verification_tests; mod identity_verification_tests;
mod integration; mod integration;
mod ip_addr_extractors;
mod ldap_basic; mod ldap_basic;
mod mtls_test; mod mtls_test;
mod oauth2_test; mod oauth2_test;

View file

@ -10,7 +10,7 @@ async fn test_v1_person_id_patch(rsclient: &KanidmClient) {
.await; .await;
assert!(res.is_ok()); assert!(res.is_ok());
create_user(&rsclient, "foo", "foogroup").await; create_user(rsclient, "foo", "foogroup").await;
let post_body = serde_json::json!({"attrs": { ATTR_MAIL : ["crab@example.com"]}}); let post_body = serde_json::json!({"attrs": { ATTR_MAIL : ["crab@example.com"]}});
@ -31,7 +31,7 @@ async fn test_v1_person_id_ssh_pubkeys_post(rsclient: &KanidmClient) {
.await; .await;
assert!(res.is_ok()); assert!(res.is_ok());
create_user(&rsclient, "foo", "foogroup").await; create_user(rsclient, "foo", "foogroup").await;
let post_body = serde_json::json!([ let post_body = serde_json::json!([
"ssh-key-tag-goes-here", "ssh-key-tag-goes-here",

View file

@ -1366,7 +1366,7 @@ async fn setup_demo_account_password(
#[kanidmd_testkit::test] #[kanidmd_testkit::test]
async fn test_server_credential_update_session_passkey(rsclient: &KanidmClient) { async fn test_server_credential_update_session_passkey(rsclient: &KanidmClient) {
let mut wa = setup_demo_account_passkey(&rsclient).await; let mut wa = setup_demo_account_passkey(rsclient).await;
let res = rsclient let res = rsclient
.auth_passkey_begin("demo_account") .auth_passkey_begin("demo_account")
@ -1690,7 +1690,7 @@ async fn test_server_user_auth_token_lifecycle(rsclient: &KanidmClient) {
#[kanidmd_testkit::test] #[kanidmd_testkit::test]
async fn test_server_user_auth_reauthentication(rsclient: &KanidmClient) { async fn test_server_user_auth_reauthentication(rsclient: &KanidmClient) {
let mut wa = setup_demo_account_passkey(&rsclient).await; let mut wa = setup_demo_account_passkey(rsclient).await;
let res = rsclient let res = rsclient
.auth_passkey_begin("demo_account") .auth_passkey_begin("demo_account")
@ -1869,12 +1869,12 @@ async fn start_password_session(
#[kanidmd_testkit::test] #[kanidmd_testkit::test]
async fn test_server_user_auth_unprivileged(rsclient: &KanidmClient) { async fn test_server_user_auth_unprivileged(rsclient: &KanidmClient) {
let (account_name, account_pass) = setup_demo_account_password(&rsclient) let (account_name, account_pass) = setup_demo_account_password(rsclient)
.await .await
.expect("Failed to setup demo_account"); .expect("Failed to setup demo_account");
let uat = start_password_session( let uat = start_password_session(
&rsclient, rsclient,
account_name.as_str(), account_name.as_str(),
account_pass.as_str(), account_pass.as_str(),
false, false,
@ -1892,18 +1892,13 @@ async fn test_server_user_auth_unprivileged(rsclient: &KanidmClient) {
#[kanidmd_testkit::test] #[kanidmd_testkit::test]
async fn test_server_user_auth_privileged_shortcut(rsclient: &KanidmClient) { async fn test_server_user_auth_privileged_shortcut(rsclient: &KanidmClient) {
let (account_name, account_pass) = setup_demo_account_password(&rsclient) let (account_name, account_pass) = setup_demo_account_password(rsclient)
.await .await
.expect("Failed to setup demo_account"); .expect("Failed to setup demo_account");
let uat = start_password_session( let uat = start_password_session(rsclient, account_name.as_str(), account_pass.as_str(), true)
&rsclient, .await
account_name.as_str(), .expect("Failed to start session");
account_pass.as_str(),
true,
)
.await
.expect("Failed to start session");
match uat.purpose { match uat.purpose {
UatPurpose::ReadOnly => panic!("Unexpected uat purpose"), UatPurpose::ReadOnly => panic!("Unexpected uat purpose"),

View file

@ -6,10 +6,10 @@ use kanidmd_testkit::*;
async fn account_id_unix_token(rsclient: &KanidmClient) { async fn account_id_unix_token(rsclient: &KanidmClient) {
login_put_admin_idm_admins(rsclient).await; login_put_admin_idm_admins(rsclient).await;
create_user(&rsclient, "group_manager", "idm_group_manage_priv").await; create_user(rsclient, "group_manager", "idm_group_manage_priv").await;
// create test user without creating new groups // create test user without creating new groups
create_user(&rsclient, NOT_ADMIN_TEST_USERNAME, NAME_IDM_ADMINS).await; create_user(rsclient, NOT_ADMIN_TEST_USERNAME, NAME_IDM_ADMINS).await;
login_account(&rsclient, "group_manager").await; login_account(rsclient, "group_manager").await;
let response = rsclient let response = rsclient
.idm_account_unix_token_get(NOT_ADMIN_TEST_USERNAME) .idm_account_unix_token_get(NOT_ADMIN_TEST_USERNAME)
@ -32,7 +32,7 @@ async fn account_id_unix_token(rsclient: &KanidmClient) {
assert!(format!("{:?}", val).contains("400")); assert!(format!("{:?}", val).contains("400"));
} }
login_put_admin_idm_admins(&rsclient).await; login_put_admin_idm_admins(rsclient).await;
rsclient rsclient
.idm_person_account_unix_extend(NOT_ADMIN_TEST_USERNAME, None, None) .idm_person_account_unix_extend(NOT_ADMIN_TEST_USERNAME, None, None)

View file

@ -5,6 +5,7 @@ use crate::{handle_client_error, PwBadlistOpt};
use std::fs::File; use std::fs::File;
use std::io::Read; use std::io::Read;
use tokio::task; use tokio::task;
use zxcvbn::Score;
const CHUNK_SIZE: usize = 1000; const CHUNK_SIZE: usize = 1000;
@ -67,10 +68,6 @@ impl PwBadlistOpt {
info!("Have {} unique passwords to process", pwset.len()); info!("Have {} unique passwords to process", pwset.len());
// Break the list into chunks per thread availability // Break the list into chunks per thread availability
// let par_count = thread::available_parallelism()
// .expect("Failed to determine available parallelism")
// .get();
let task_handles: Vec<_> = pwset let task_handles: Vec<_> = pwset
.chunks(CHUNK_SIZE) .chunks(CHUNK_SIZE)
.map(|chunk| chunk.to_vec()) .map(|chunk| chunk.to_vec())
@ -82,18 +79,7 @@ impl PwBadlistOpt {
if v.len() < 10 { if v.len() < 10 {
return false; return false;
} }
match zxcvbn::zxcvbn(v.as_str(), &[]) { zxcvbn::zxcvbn(v.as_str(), &[]).score() >= Score::Four
Ok(r) => r.score() >= 4,
Err(e) => {
error!(
"zxcvbn unable to process '{}' - {:?}",
v.as_str(),
e
);
error!("adding to badlist anyway ...");
true
}
}
}) })
.map(|s| s.to_string()) .map(|s| s.to_string())
.collect::<Vec<_>>(); .collect::<Vec<_>>();

View file

@ -8,4 +8,6 @@ pub enum Error {
Interrupt, Interrupt,
Crossbeam, Crossbeam,
InvalidState, InvalidState,
#[allow(dead_code)]
RandomNumber(String),
} }

View file

@ -4,8 +4,9 @@ use crate::model::ActorRole;
use crate::profile::Profile; use crate::profile::Profile;
use crate::state::{Credential, Flag, Group, GroupName, Person, PreflightState, State}; use crate::state::{Credential, Flag, Group, GroupName, Person, PreflightState, State};
use hashbrown::HashMap; use hashbrown::HashMap;
use rand::distributions::{Alphanumeric, DistString, Uniform}; use rand::distr::{Alphanumeric, SampleString, Uniform};
use rand::seq::{index, SliceRandom}; use rand::seq::{index, IndexedRandom};
use rand::{Rng, SeedableRng}; use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng; use rand_chacha::ChaCha8Rng;
@ -171,7 +172,8 @@ pub async fn populate(_client: &KanidmOrcaClient, profile: Profile) -> Result<St
let baseline = persons.len() / 3; let baseline = persons.len() / 3;
let inverse = persons.len() - baseline; let inverse = persons.len() - baseline;
// Randomly add extra from the inverse // Randomly add extra from the inverse
let extra = Uniform::new(0, inverse); let extra =
Uniform::new(0, inverse).map_err(|err| Error::RandomNumber(err.to_string()))?;
baseline + seeded_rng.sample(extra) baseline + seeded_rng.sample(extra)
} }
}; };

View file

@ -27,7 +27,7 @@ impl ActorBasic {
pub fn new(mut cha_rng: ChaCha8Rng, warmup_time_ms: u64) -> Self { pub fn new(mut cha_rng: ChaCha8Rng, warmup_time_ms: u64) -> Self {
let max_backoff_time_in_ms = 2 * warmup_time_ms / 3; let max_backoff_time_in_ms = 2 * warmup_time_ms / 3;
let randomised_backoff_time = let randomised_backoff_time =
Duration::from_millis(cha_rng.gen_range(0..max_backoff_time_in_ms)); Duration::from_millis(cha_rng.random_range(0..max_backoff_time_in_ms));
ActorBasic { ActorBasic {
state: State::Unauthenticated, state: State::Unauthenticated,
randomised_backoff_time, randomised_backoff_time,

View file

@ -76,7 +76,7 @@ impl ActorLatencyMeasurer {
let max_backoff_time_in_ms = 2 * warmup_time_ms / 3; let max_backoff_time_in_ms = 2 * warmup_time_ms / 3;
let randomised_backoff_time = let randomised_backoff_time =
Duration::from_millis(cha_rng.gen_range(0..max_backoff_time_in_ms)); Duration::from_millis(cha_rng.random_range(0..max_backoff_time_in_ms));
Ok(ActorLatencyMeasurer { Ok(ActorLatencyMeasurer {
state: State::Unauthenticated, state: State::Unauthenticated,
randomised_backoff_time, randomised_backoff_time,

View file

@ -25,7 +25,7 @@ impl ActorReader {
pub fn new(mut cha_rng: ChaCha8Rng, warmup_time_ms: u64) -> Self { pub fn new(mut cha_rng: ChaCha8Rng, warmup_time_ms: u64) -> Self {
let max_backoff_time_in_ms = warmup_time_ms - 1000; let max_backoff_time_in_ms = warmup_time_ms - 1000;
let randomised_backoff_time = let randomised_backoff_time =
Duration::from_millis(cha_rng.gen_range(0..max_backoff_time_in_ms)); Duration::from_millis(cha_rng.random_range(0..max_backoff_time_in_ms));
ActorReader { ActorReader {
state: State::Unauthenticated, state: State::Unauthenticated,
randomised_backoff_time, randomised_backoff_time,

View file

@ -26,7 +26,7 @@ impl ActorWriter {
pub fn new(mut cha_rng: ChaCha8Rng, warmup_time_ms: u64) -> Self { pub fn new(mut cha_rng: ChaCha8Rng, warmup_time_ms: u64) -> Self {
let max_backoff_time_in_ms = 2 * warmup_time_ms / 3; let max_backoff_time_in_ms = 2 * warmup_time_ms / 3;
let randomised_backoff_time = let randomised_backoff_time =
Duration::from_millis(cha_rng.gen_range(0..max_backoff_time_in_ms)); Duration::from_millis(cha_rng.random_range(0..max_backoff_time_in_ms));
ActorWriter { ActorWriter {
state: State::Unauthenticated, state: State::Unauthenticated,
randomised_backoff_time, randomised_backoff_time,

View file

@ -1,6 +1,6 @@
use crate::error::Error; use crate::error::Error;
use crate::state::{GroupName, Model}; use crate::state::{GroupName, Model};
use rand::{thread_rng, Rng}; use rand::{rng, Rng};
use serde::de::{value, IntoDeserializer}; use serde::de::{value, IntoDeserializer};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::BTreeMap; use std::collections::BTreeMap;
@ -200,8 +200,8 @@ impl ProfileBuilder {
} = self; } = self;
let seed: u64 = seed.unwrap_or_else(|| { let seed: u64 = seed.unwrap_or_else(|| {
let mut rng = thread_rng(); let mut rng = rng();
rng.gen() rng.random()
}); });
//TODO: Allow to specify group properties from the CLI //TODO: Allow to specify group properties from the CLI

View file

@ -187,7 +187,7 @@ pub async fn execute(state: State, control_rx: broadcast::Receiver<Signal>) -> R
}) })
}) })
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
let main_client_index = seeded_rng.gen_range(0..cloned_clients.len()); let main_client_index = seeded_rng.random_range(0..cloned_clients.len());
let main_client = cloned_clients.remove(main_client_index); let main_client = cloned_clients.remove(main_client_index);
//note that cloned_clients now contains all other clients except the first one //note that cloned_clients now contains all other clients except the first one

View file

@ -694,7 +694,7 @@ mod tests {
for file in PathBuf::from(&examples_dir) for file in PathBuf::from(&examples_dir)
.canonicalize() .canonicalize()
.expect(&format!("Can't find examples dir at {}", examples_dir)) .unwrap_or_else(|_| panic!("Can't find examples dir at {}", examples_dir))
.read_dir() .read_dir()
.expect("Can't read examples dir!") .expect("Can't read examples dir!")
{ {

View file

@ -71,32 +71,32 @@ impl RequestOptions {
fn nss_fallback_unavail() { fn nss_fallback_unavail() {
let req_opt = RequestOptions::fallback_unavail(); let req_opt = RequestOptions::fallback_unavail();
let Response::Unavail = core::get_all_user_entries(req_opt) else { let Response::Unavail = core::get_all_user_entries(req_opt) else {
unreachable!(); panic!("unrecoverable");
}; };
let req_opt = RequestOptions::fallback_unavail(); let req_opt = RequestOptions::fallback_unavail();
let Response::Unavail = core::get_user_entry_by_uid(0, req_opt) else { let Response::Unavail = core::get_user_entry_by_uid(0, req_opt) else {
unreachable!(); panic!("unrecoverable");
}; };
let req_opt = RequestOptions::fallback_unavail(); let req_opt = RequestOptions::fallback_unavail();
let Response::Unavail = core::get_user_entry_by_name("root".to_string(), req_opt) else { let Response::Unavail = core::get_user_entry_by_name("root".to_string(), req_opt) else {
unreachable!(); panic!("unrecoverable");
}; };
let req_opt = RequestOptions::fallback_unavail(); let req_opt = RequestOptions::fallback_unavail();
let Response::Unavail = core::get_all_group_entries(req_opt) else { let Response::Unavail = core::get_all_group_entries(req_opt) else {
unreachable!(); panic!("unrecoverable");
}; };
let req_opt = RequestOptions::fallback_unavail(); let req_opt = RequestOptions::fallback_unavail();
let Response::Unavail = core::get_group_entry_by_gid(0, req_opt) else { let Response::Unavail = core::get_group_entry_by_gid(0, req_opt) else {
unreachable!(); panic!("unrecoverable");
}; };
let req_opt = RequestOptions::fallback_unavail(); let req_opt = RequestOptions::fallback_unavail();
let Response::Unavail = core::get_group_entry_by_name("root".to_string(), req_opt) else { let Response::Unavail = core::get_group_entry_by_name("root".to_string(), req_opt) else {
unreachable!(); panic!("unrecoverable");
}; };
} }
@ -105,7 +105,7 @@ fn nss_fallback_all_user_entries() {
let req_opt = RequestOptions::fallback_fixture(); let req_opt = RequestOptions::fallback_fixture();
let Response::Success(users) = core::get_all_user_entries(req_opt) else { let Response::Success(users) = core::get_all_user_entries(req_opt) else {
unreachable!(); panic!("Failed to get all user entries");
}; };
assert_eq!(users.len(), 3); assert_eq!(users.len(), 3);
@ -129,7 +129,7 @@ fn nss_fallback_all_user_entries() {
fn nss_fallback_user_entry_by_uid() { fn nss_fallback_user_entry_by_uid() {
let req_opt = RequestOptions::fallback_fixture(); let req_opt = RequestOptions::fallback_fixture();
let Response::Success(user) = core::get_user_entry_by_uid(0, req_opt) else { let Response::Success(user) = core::get_user_entry_by_uid(0, req_opt) else {
unreachable!(); panic!("Failed to get user entry by uid");
}; };
assert_eq!(user.name, "root"); assert_eq!(user.name, "root");
@ -139,7 +139,7 @@ fn nss_fallback_user_entry_by_uid() {
let req_opt = RequestOptions::fallback_fixture(); let req_opt = RequestOptions::fallback_fixture();
let Response::Success(user) = core::get_user_entry_by_uid(1000, req_opt) else { let Response::Success(user) = core::get_user_entry_by_uid(1000, req_opt) else {
unreachable!(); panic!("Failed to get user entry by uid");
}; };
assert_eq!(user.name, "tobias"); assert_eq!(user.name, "tobias");
@ -149,7 +149,7 @@ fn nss_fallback_user_entry_by_uid() {
let req_opt = RequestOptions::fallback_fixture(); let req_opt = RequestOptions::fallback_fixture();
let Response::NotFound = core::get_user_entry_by_uid(10, req_opt) else { let Response::NotFound = core::get_user_entry_by_uid(10, req_opt) else {
unreachable!(); panic!("Wrong result");
}; };
} }
@ -157,7 +157,7 @@ fn nss_fallback_user_entry_by_uid() {
fn nss_fallback_user_entry_by_name() { fn nss_fallback_user_entry_by_name() {
let req_opt = RequestOptions::fallback_fixture(); let req_opt = RequestOptions::fallback_fixture();
let Response::Success(user) = core::get_user_entry_by_name("root".to_string(), req_opt) else { let Response::Success(user) = core::get_user_entry_by_name("root".to_string(), req_opt) else {
unreachable!(); panic!("Failed to get user entry by name");
}; };
assert_eq!(user.name, "root"); assert_eq!(user.name, "root");
@ -167,7 +167,7 @@ fn nss_fallback_user_entry_by_name() {
let req_opt = RequestOptions::fallback_fixture(); let req_opt = RequestOptions::fallback_fixture();
let Response::Success(user) = core::get_user_entry_by_name("ellie".to_string(), req_opt) else { let Response::Success(user) = core::get_user_entry_by_name("ellie".to_string(), req_opt) else {
unreachable!(); panic!("Failed to get user entry by name");
}; };
assert_eq!(user.name, "ellie"); assert_eq!(user.name, "ellie");
@ -177,7 +177,7 @@ fn nss_fallback_user_entry_by_name() {
let req_opt = RequestOptions::fallback_fixture(); let req_opt = RequestOptions::fallback_fixture();
let Response::NotFound = core::get_user_entry_by_name("william".to_string(), req_opt) else { let Response::NotFound = core::get_user_entry_by_name("william".to_string(), req_opt) else {
unreachable!(); panic!("Wrong result");
}; };
} }
@ -186,7 +186,7 @@ fn nss_fallback_all_group_entries() {
let req_opt = RequestOptions::fallback_fixture(); let req_opt = RequestOptions::fallback_fixture();
let Response::Success(groups) = core::get_all_group_entries(req_opt) else { let Response::Success(groups) = core::get_all_group_entries(req_opt) else {
unreachable!(); panic!("Failed to get all group entries");
}; };
assert_eq!(groups.len(), 3); assert_eq!(groups.len(), 3);
@ -207,7 +207,7 @@ fn nss_fallback_all_group_entries() {
fn nss_fallback_group_entry_by_uid() { fn nss_fallback_group_entry_by_uid() {
let req_opt = RequestOptions::fallback_fixture(); let req_opt = RequestOptions::fallback_fixture();
let Response::Success(group) = core::get_group_entry_by_gid(0, req_opt) else { let Response::Success(group) = core::get_group_entry_by_gid(0, req_opt) else {
unreachable!(); panic!("Failed to get group entry by gid");
}; };
assert_eq!(group.name, "root"); assert_eq!(group.name, "root");
@ -216,7 +216,7 @@ fn nss_fallback_group_entry_by_uid() {
let req_opt = RequestOptions::fallback_fixture(); let req_opt = RequestOptions::fallback_fixture();
let Response::Success(group) = core::get_group_entry_by_gid(1000, req_opt) else { let Response::Success(group) = core::get_group_entry_by_gid(1000, req_opt) else {
unreachable!(); panic!("Failed to get group entry by gid");
}; };
assert_eq!(group.name, "tobias"); assert_eq!(group.name, "tobias");
@ -225,7 +225,7 @@ fn nss_fallback_group_entry_by_uid() {
let req_opt = RequestOptions::fallback_fixture(); let req_opt = RequestOptions::fallback_fixture();
let Response::NotFound = core::get_group_entry_by_gid(10, req_opt) else { let Response::NotFound = core::get_group_entry_by_gid(10, req_opt) else {
unreachable!(); panic!("Wrong result");
}; };
} }
@ -234,7 +234,7 @@ fn nss_fallback_group_entry_by_name() {
let req_opt = RequestOptions::fallback_fixture(); let req_opt = RequestOptions::fallback_fixture();
let Response::Success(group) = core::get_group_entry_by_name("root".to_string(), req_opt) let Response::Success(group) = core::get_group_entry_by_name("root".to_string(), req_opt)
else { else {
unreachable!(); panic!("Failed to get group entry by name");
}; };
assert_eq!(group.name, "root"); assert_eq!(group.name, "root");
@ -244,7 +244,7 @@ fn nss_fallback_group_entry_by_name() {
let req_opt = RequestOptions::fallback_fixture(); let req_opt = RequestOptions::fallback_fixture();
let Response::Success(group) = core::get_group_entry_by_name("ellie".to_string(), req_opt) let Response::Success(group) = core::get_group_entry_by_name("ellie".to_string(), req_opt)
else { else {
unreachable!(); panic!("Failed to get group entry by name");
}; };
assert_eq!(group.name, "ellie"); assert_eq!(group.name, "ellie");
@ -253,6 +253,6 @@ fn nss_fallback_group_entry_by_name() {
let req_opt = RequestOptions::fallback_fixture(); let req_opt = RequestOptions::fallback_fixture();
let Response::NotFound = core::get_group_entry_by_name("william".to_string(), req_opt) else { let Response::NotFound = core::get_group_entry_by_name("william".to_string(), req_opt) else {
unreachable!(); panic!("Wrong result");
}; };
} }

View file

@ -121,23 +121,17 @@ impl PamHandler for TestHandler {
/// Display a message to the user. /// Display a message to the user.
fn message(&self, _prompt: &str) -> PamResult<()> { fn message(&self, _prompt: &str) -> PamResult<()> {
let mut q = self.response_queue.lock().unwrap(); let mut q = self.response_queue.lock().unwrap();
match q.pop_front() { let e = q.pop_front();
e => { eprintln!("{:?}", e);
eprintln!("{:?}", e); panic!("Invalid event transition message");
panic!("Invalid event transition");
}
}
} }
/// Display a device grant request to the user. /// Display a device grant request to the user.
fn message_device_grant(&self, _data: &DeviceAuthorizationResponse) -> PamResult<()> { fn message_device_grant(&self, _data: &DeviceAuthorizationResponse) -> PamResult<()> {
let mut q = self.response_queue.lock().unwrap(); let mut q = self.response_queue.lock().unwrap();
match q.pop_front() { let e = q.pop_front();
e => { eprintln!("{:?}", e);
eprintln!("{:?}", e); panic!("Invalid event transition message_device_grant");
panic!("Invalid event transition");
}
}
} }
/// Request a password from the user. /// Request a password from the user.
@ -154,22 +148,16 @@ impl PamHandler for TestHandler {
fn prompt_for_pin(&self, _msg: Option<&str>) -> PamResult<Option<String>> { fn prompt_for_pin(&self, _msg: Option<&str>) -> PamResult<Option<String>> {
let mut q = self.response_queue.lock().unwrap(); let mut q = self.response_queue.lock().unwrap();
match q.pop_front() { let e = q.pop_front();
e => { eprintln!("{:?}", e);
eprintln!("{:?}", e); panic!("Invalid event transition prompt_for_pin");
panic!("Invalid event transition");
}
}
} }
fn prompt_for_mfacode(&self) -> PamResult<Option<String>> { fn prompt_for_mfacode(&self) -> PamResult<Option<String>> {
let mut q = self.response_queue.lock().unwrap(); let mut q = self.response_queue.lock().expect("Failed to lock mutex");
match q.pop_front() { let e = q.pop_front();
e => { eprintln!("{:?}", e);
eprintln!("{:?}", e); panic!("Invalid event transition prompt_for_mfacode");
panic!("Invalid event transition");
}
}
} }
} }

View file

@ -1189,7 +1189,7 @@ async fn test_cache_extend_group_members() {
assert!(groups.iter().any(|group| { assert!(groups.iter().any(|group| {
group.name == "extensible_group" group.name == "extensible_group"
&& group.members.as_slice() && group.members.as_slice()
== &[ == [
"local_account".to_string(), "local_account".to_string(),
"testaccount1@idm.example.com".to_string(), "testaccount1@idm.example.com".to_string(),
] ]