Compare commits

...

8 commits

Author SHA1 Message Date
Fabian Kammel 6c79eb5f22
Merge d0014e547a into be4818e121 2025-04-24 10:45:21 +01:00
James Hodgkinson be4818e121
Update dependencies, fix a bunch of clippy lints () 2025-04-24 11:25:25 +10:00
Firstyear 5201ec11e8
Support spaces in ssh key comments () 2025-04-23 13:23:22 +10:00
Firstyear e97f4bc54c
20250402 3423 proxy protocol ()
Implement the PROXY protocol for IP address information from load balancers. This improves our handling and configuration of x-forward-for as well to include trusted IP ranges.
2025-04-23 01:10:01 +00:00
Fabian Kammel d0014e547a Link to official installation docs instead
Signed-off-by: Fabian Kammel <fabian@kammel.dev>
2025-04-18 16:22:21 +02:00
Fabian Kammel 95ff67ddb4
Apply suggestions from code review
Co-authored-by: James Hodgkinson <james@terminaloutcomes.com>
2025-04-18 16:13:39 +02:00
Fabian Kammel 1e1ed6caba docs: update to OPKSSH version 0.5.1 with support for ES256
Signed-off-by: Fabian Kammel <fabian@kammel.dev>
2025-04-16 21:45:55 +02:00
Fabian Kammel 2986026647 docs: document how to configure oauth2 for opkssh
Signed-off-by: Fabian Kammel <fabian@kammel.dev>
2025-04-16 19:14:45 +02:00
79 changed files with 1701 additions and 948 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" }
# 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" }
# Allow ssh keys to have comments with spaces.
sshkeys = { git = "https://github.com/Firstyear/rust-sshkeys.git", rev = "3a081cbf7480628223bcb96fc8aaa8c19109d007" }
[workspace.dependencies]
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" }
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"] }
askama = { version = "0.12.1", features = ["serde", "with-axum"] }
askama_axum = { version = "0.4.0" }
@ -159,7 +161,7 @@ base64 = "^0.22.1"
base64urlsafedata = "0.5.1"
bitflags = "^2.8.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"
# Forced by saffron/cron
chrono = "^0.4.39"
@ -176,16 +178,18 @@ filetime = "^0.2.24"
fs4 = "^0.13.0"
futures = "^0.3.31"
futures-util = { version = "^0.3.30", features = ["sink"] }
gix = { version = "0.64.0", default-features = false }
hashbrown = { version = "0.14.3", features = ["serde", "inline-more", "ahash"] }
gix = { version = "0.71.0", default-features = false }
haproxy-protocol = { version = "0.0.1" }
hashbrown = { version = "0.15.2", features = ["serde", "inline-more"] }
hex = "^0.4.3"
http = "1.2.0"
http-body-util = "0.1"
hyper = { version = "1.5.1", features = [
"full",
] } # hyper full includes client/server/http2
hyper-util = { version = "0.1.10", features = ["server", "tokio"] }
idlset = "^0.2.5"
image = { version = "0.24.9", default-features = false, features = [
image = { version = "0.25.6", default-features = false, features = [
"gif",
"jpeg",
"webp",
@ -198,16 +202,16 @@ lazy_static = "^1.5.0"
ldap3_client = "^0.5.2"
ldap3_proto = { version = "^0.5.2", features = ["serde"] }
libc = "^0.2.168"
libc = "0.2.172"
libnss = "^0.8.0"
libsqlite3-sys = "^0.25.2"
lodepng = "3.11.0"
lru = "^0.13.0"
mathru = "^0.13.0"
lru = "0.14.0"
mathru = "0.15.5"
md-5 = "0.10.6"
mimalloc = "0.1.43"
mimalloc = "0.1.46"
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 }
openssl-sys = "^0.9"
openssl = "^0.10.72"
@ -229,11 +233,11 @@ tracing-core = "0.1.33"
peg = "0.8"
pkg-config = "^0.3.31"
prctl = "1.0.0"
proc-macro2 = "1.0.93"
qrcode = "^0.12.0"
proc-macro2 = "1.0.95"
qrcode = "0.14.1"
quote = "1"
rand = "^0.8.5"
rand_chacha = "0.3.1"
rand = "0.9.1"
rand_chacha = "0.9.0"
regex = "1.11.0"
reqwest = { version = "0.12.12", default-features = false, features = [
"cookies",
@ -243,13 +247,13 @@ reqwest = { version = "0.12.12", default-features = false, features = [
"rustls-tls-native-roots",
"rustls-tls-native-roots-no-provider",
] }
rusqlite = { version = "^0.28.0", features = ["array", "bundled"] }
rustls = { version = "0.23.21", default-features = false, features = [
rusqlite = { version = "0.35.0", features = ["array", "bundled"] }
rustls = { version = "0.23.26", default-features = false, features = [
"aws_lc_rs",
] }
sd-notify = "^0.4.5"
selinux = "^0.4.6"
selinux = "^0.5.1"
serde = "^1.0.217"
serde_cbor = { version = "0.12.0-dev", package = "serde_cbor_2" }
serde_json = "^1.0.137"
@ -257,13 +261,13 @@ serde_urlencoded = "^0.7.1"
serde_with = "3.12.0"
sha-crypt = "0.5.0"
sha2 = "0.10.8"
shellexpand = "^2.1.2"
shellexpand = "3.1.1"
smartstring = "^1.0.1"
smolset = "^1.3.1"
sshkey-attest = "^0.5.0"
sshkeys = "0.3.3"
svg = "0.13.1"
syn = { version = "2.0.96", features = ["full"] }
svg = "0.18.0"
syn = { version = "2.0.100", features = ["full"] }
tempfile = "3.15.0"
testkit-macros = { path = "./server/testkit-macros" }
time = { version = "^0.3.36", features = ["formatting", "local-offset"] }
@ -272,7 +276,7 @@ tokio = "^1.44.2"
tokio-openssl = "^0.6.5"
tokio-util = "^0.7.13"
toml = "^0.5.11"
toml = "^0.8.20"
tracing = { version = "^0.1.41", features = [
"max_level_trace",
"release_max_level_debug",
@ -299,6 +303,6 @@ walkdir = "2"
x509-cert = "0.2.5"
zxcvbn = "^2.2.2"
zxcvbn = "3.1.0"
nonempty = "0.8.1"
nonempty = "0.11.0"

View file

@ -562,7 +562,7 @@ OAuth2 Proxy is a reverse proxy that provides authentication with OpenID Connect
It is typically used to secure web applications without native OpenID Connect support.
Prepare the environment.
Due to a [lack of public client support](https://github.com/oauth2-proxy/oauth2-proxy/issues/1714) we have to set it up as a basic client.
Due to a [lack of public client support](https://github.com/oauth2-proxy/oauth2-proxy/issues/1714) we have to set it up as a basic client.
```bash
kanidm system oauth2 create webapp 'webapp.example.com' 'https://webapp.example.com'
@ -615,6 +615,73 @@ allowed_groups = ["webapp_admin"]
client_secret = "<SECRET>"
```
## OPKSSH
[OPKSSH](https://github.com/openpubkey/opkssh) is a tool of the
[OpenPubkey](https://github.com/openpubkey/openpubkey) project. It enables SSH
to be used with OpenID Connect allowing access to be managed via identities
like `alice@example.com` instead of long-lived private keys. It does not replace SSH,
but instead generates private keys on the fly, and augments the verification process
on the server side.
To set up OPKSSH to authenticate with Kanidm:
1. Add an email address to your regular Kanidm account, if it doesn't have one
already:
```sh
kanidm person update alice -m alice@example.com
```
2. Create a new Kanidm group for your OPKSSH users (`opkssh_users`), and add your
regular account to it:
```sh
kanidm group create opkssh_users
kanidm group add-members opkssh_users alice
```
3. Create a new OAuth2 application configuration in Kanidm (`opkssh`), configure
the redirect URL, and scope access to the `opkssh_users` group:
```sh
# The redirect origin is set to localhost for local callbacks
kanidm system oauth2 create-public opkssh opkssh http://localhost:3000
# Add the specific redirect URIs used by OPKSSH
kanidm system oauth2 add-redirect-url opkssh http://localhost:3000/login-callback
kanidm system oauth2 add-redirect-url opkssh http://localhost:10001/login-callback
kanidm system oauth2 add-redirect-url opkssh http://localhost:11110/login-callback
# Explicitly allow localhost redirects for this client
kanidm system oauth2 enable-localhost-redirects opkssh
# Map the group created earlier to the required OIDC scopes
kanidm system oauth2 update-scope-map opkssh opkssh_users email openid profile groups
```
4. On the SSH server side, [install opkssh](https://github.com/openpubkey/opkssh#installing-on-a-server)
and allow your user to connect via:
```sh
# where 'user' is the linux user
sudo opkssh add user alice@example.com https://idm.example.com/oauth2/openid/opkssh
```
5. On the SSH client side, [install opkssh](https://github.com/openpubkey/opkssh#getting-started)
and login via Kanidm:
```sh
opkssh login --provider=https://idm.example.com/oauth2/openid/opkssh,opkssh
```
6. Use SSH as you would normally:
```sh
ssh user@your-server-hostname
```
## Outline
> These instructions were tested with self-hosted Outline 0.80.2.

View file

@ -13,16 +13,6 @@ bindaddress = "[::]:443"
# Defaults to "" (disabled)
# 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.
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: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]
# The path to the output folder for online backups
path = "/var/lib/private/kanidm/backups/"

View file

@ -13,16 +13,6 @@ bindaddress = "[::]:8443"
# Defaults to "" (disabled)
# 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.
db_path = "/data/kanidm.db"
#
@ -85,7 +75,32 @@ domain = "idm.example.com"
# not consistent, the server WILL refuse to start!
# origin = "https://idm.example.com"
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]
# The path to the output folder for online backups
path = "/data/kanidm/backups/"

View file

@ -834,9 +834,9 @@ impl TryFrom<&str> for Password {
impl Password {
fn bench_pbkdf2(pbkdf2_cost: usize) -> Option<Duration> {
let mut rng = rand::thread_rng();
let salt: Vec<u8> = (0..PBKDF2_SALT_LEN).map(|_| rng.gen()).collect();
let input: Vec<u8> = (0..PBKDF2_SALT_LEN).map(|_| rng.gen()).collect();
let mut rng = rand::rng();
let salt: Vec<u8> = (0..PBKDF2_SALT_LEN).map(|_| rng.random()).collect();
let input: Vec<u8> = (0..PBKDF2_SALT_LEN).map(|_| rng.random()).collect();
// This is 512 bits of output
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> {
let mut rng = rand::thread_rng();
let salt: Vec<u8> = (0..ARGON2_SALT_LEN).map(|_| rng.gen()).collect();
let input: Vec<u8> = (0..ARGON2_SALT_LEN).map(|_| rng.gen()).collect();
let mut rng = rand::rng();
let salt: Vec<u8> = (0..ARGON2_SALT_LEN).map(|_| rng.random()).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 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> {
let pbkdf2_cost = policy.pbkdf2_cost;
let mut rng = rand::thread_rng();
let salt: Vec<u8> = (0..PBKDF2_SALT_LEN).map(|_| rng.gen()).collect();
let mut rng = rand::rng();
let salt: Vec<u8> = (0..PBKDF2_SALT_LEN).map(|_| rng.random()).collect();
let mut key: Vec<u8> = (0..PBKDF2_KEY_LEN).map(|_| 0).collect();
pbkdf2_hmac(
@ -897,8 +897,8 @@ impl Password {
let argon = Argon2::new(Algorithm::Argon2id, version, policy.argon2id_params.clone());
let mut rng = rand::thread_rng();
let salt: Vec<u8> = (0..ARGON2_SALT_LEN).map(|_| rng.gen()).collect();
let mut rng = rand::rng();
let salt: Vec<u8> = (0..ARGON2_SALT_LEN).map(|_| rng.random()).collect();
let mut key: Vec<u8> = (0..ARGON2_KEY_LEN).map(|_| 0).collect();
argon
@ -925,8 +925,8 @@ impl Password {
let argon = Argon2::new(Algorithm::Argon2id, version, policy.argon2id_params.clone());
let mut rng = rand::thread_rng();
let salt: Vec<u8> = (0..ARGON2_SALT_LEN).map(|_| rng.gen()).collect();
let mut rng = rand::rng();
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();
argon

View file

@ -77,7 +77,10 @@ pub fn apply_profile() {
.decode(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));
// 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 }
futures = { workspace = true }
futures-util = { workspace = true }
haproxy-protocol = { workspace = true, features = ["tokio"] }
hashbrown = { workspace = true }
hyper = { workspace = true }
hyper-util = { 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
//! or domain entries that are able to be replicated.
use std::fmt::{self, Display};
use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use hashbrown::HashSet;
use kanidm_proto::constants::DEFAULT_SERVER_ADDRESS;
use kanidm_proto::internal::FsType;
use kanidm_proto::messages::ConsoleOutputMode;
use serde::Deserialize;
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 crate::repl::config::ReplicationConfiguration;
@ -100,6 +100,111 @@ pub struct TlsConfiguration {
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.
///
/// 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>,
log_level: Option<LogLevel>,
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>,
thread_count: Option<usize>,
maximum_request_size_bytes: Option<usize>,
@ -490,7 +598,10 @@ pub struct Configuration {
pub db_fs_type: Option<FsType>,
pub db_arc_size: Option<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 integration_test_config: Option<Box<IntegrationTestConfig>>,
pub online_backup: Option<OnlineBackup>,
@ -522,7 +633,8 @@ impl Configuration {
db_fs_type: None,
db_arc_size: None,
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_chain: None,
tls_client_ca: None,
@ -547,7 +659,8 @@ impl Configuration {
db_fs_type: None,
db_arc_size: None,
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,
integration_test_config: None,
online_backup: None,
@ -587,7 +700,17 @@ impl fmt::Display for Configuration {
None => write!(f, "arcsize: AUTO, "),
}?;
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())?;
match &self.online_backup {
Some(bck) => write!(
@ -642,7 +765,8 @@ pub struct ConfigurationBuilder {
db_fs_type: Option<FsType>,
db_arc_size: Option<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_chain: Option<PathBuf>,
tls_client_ca: Option<PathBuf>,
@ -691,8 +815,8 @@ impl ConfigurationBuilder {
self.db_arc_size = env_config.db_arc_size;
}
if env_config.trust_x_forward_for.is_some() {
self.trust_x_forward_for = env_config.trust_x_forward_for;
if env_config.trust_x_forward_for == Some(true) {
self.http_client_address_info = HttpAddressInfo::XForwardForAllSourcesTrusted;
}
if env_config.tls_key.is_some() {
@ -813,8 +937,8 @@ impl ConfigurationBuilder {
self.db_arc_size = config.db_arc_size;
}
if config.trust_x_forward_for.is_some() {
self.trust_x_forward_for = config.trust_x_forward_for;
if config.trust_x_forward_for == Some(true) {
self.http_client_address_info = HttpAddressInfo::XForwardForAllSourcesTrusted;
}
if config.online_backup.is_some() {
@ -893,8 +1017,12 @@ impl ConfigurationBuilder {
self.db_arc_size = config.db_arc_size;
}
if config.trust_x_forward_for.is_some() {
self.trust_x_forward_for = config.trust_x_forward_for;
if let Some(http_client_address_info) = config.http_client_address_info {
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() {
@ -930,7 +1058,8 @@ impl ConfigurationBuilder {
db_fs_type,
db_arc_size,
maximum_request,
trust_x_forward_for,
http_client_address_info,
ldap_client_address_info,
tls_key,
tls_chain,
tls_client_ca,
@ -986,7 +1115,6 @@ impl ConfigurationBuilder {
let adminbindpath =
adminbindpath.unwrap_or(env!("KANIDM_SERVER_ADMIN_BIND_PATH").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 role = role.unwrap_or(ServerRole::WriteReplica);
let log_level = log_level.unwrap_or_default();
@ -1000,7 +1128,8 @@ impl ConfigurationBuilder {
db_fs_type,
db_arc_size,
maximum_request,
trust_x_forward_for,
http_client_address_info,
ldap_client_address_info,
tls_config,
online_backup,
domain,

View file

@ -5,7 +5,6 @@ use axum::{
http::{
header::HeaderName, header::AUTHORIZATION as AUTHORISATION, request::Parts, StatusCode,
},
serve::IncomingStream,
RequestPartsExt,
};
@ -40,7 +39,8 @@ impl FromRequestParts<ServerState> for TrustedClientIp {
state: &ServerState,
) -> Result<Self, Self::Rejection> {
let ConnectInfo(ClientConnInfo {
addr,
connection_addr,
client_addr,
client_cert: _,
}) = parts
.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) {
// X forward for may be comma separated.
let first = x_forward_for
@ -75,10 +81,14 @@ impl FromRequestParts<ServerState> for TrustedClientIp {
)
})?
} else {
addr.ip()
client_addr.ip()
}
} 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))
@ -97,7 +107,11 @@ impl FromRequestParts<ServerState> for VerifiedClientInformation {
parts: &mut Parts,
state: &ServerState,
) -> Result<Self, Self::Rejection> {
let ConnectInfo(ClientConnInfo { addr, client_cert }) = parts
let ConnectInfo(ClientConnInfo {
connection_addr,
client_addr,
client_cert,
}) = parts
.extract::<ConnectInfo<ClientConnInfo>>()
.await
.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) {
// X forward for may be comma separated.
let first = x_forward_for
@ -130,10 +150,10 @@ impl FromRequestParts<ServerState> for VerifiedClientInformation {
)
})?
} else {
addr.ip()
client_addr.ip()
}
} else {
addr.ip()
client_addr.ip()
};
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)]
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
pub client_cert: Option<ClientCertInfo>,
}
// This is the normal way that our extractors get the ip info
impl Connected<ClientConnInfo> for ClientConnInfo {
fn connect_info(target: ClientConnInfo) -> Self {
target
}
}
// This is only used for plaintext http - in other words, integration tests only.
impl Connected<SocketAddr> for ClientConnInfo {
fn connect_info(addr: SocketAddr) -> Self {
fn connect_info(connection_addr: SocketAddr) -> Self {
ClientConnInfo {
addr,
client_cert: None,
}
}
}
impl Connected<IncomingStream<'_>> for ClientConnInfo {
fn connect_info(target: IncomingStream<'_>) -> Self {
ClientConnInfo {
addr: target.remote_addr(),
client_addr: connection_addr,
connection_addr,
client_cert: None,
}
}

View file

@ -17,9 +17,8 @@ mod views;
use self::extractors::ClientConnInfo;
use self::javascript::*;
use crate::actors::{QueryServerReadV1, QueryServerWriteV1};
use crate::config::{Configuration, ServerRole};
use crate::config::{AddressSet, Configuration, ServerRole};
use crate::CoreAction;
use axum::{
body::Body,
extract::connect_info::IntoMakeServiceWithConnectInfo,
@ -29,22 +28,28 @@ use axum::{
routing::*,
Router,
};
use axum_extra::extract::cookie::CookieJar;
use compact_jwt::{error::JwtError, JwsCompact, JwsHs256Signer, JwsVerifier};
use futures::pin_mut;
use haproxy_protocol::{ProxyHdrV2, RemoteAddress};
use hashbrown::HashSet;
use hyper::body::Incoming;
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 kanidmd_lib::{idm::ClientCertInfo, status::StatusActor};
use openssl::ssl::{Ssl, SslAcceptor};
use kanidm_lib_crypto::x509_cert::{der::Decode, x509_public_key_s256, Certificate};
use serde::de::DeserializeOwned;
use sketching::*;
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::{
io::{AsyncRead, AsyncWrite},
net::{TcpListener, TcpStream},
sync::broadcast,
sync::mpsc,
@ -56,11 +61,6 @@ use tower_http::{services::ServeDir, trace::TraceLayer};
use url::Url;
use uuid::Uuid;
use std::io::ErrorKind;
use std::path::PathBuf;
use std::pin::Pin;
use std::{net::SocketAddr, str::FromStr};
#[derive(Clone)]
pub struct ServerState {
pub(crate) status_ref: &'static StatusActor,
@ -68,7 +68,7 @@ pub struct ServerState {
pub(crate) qe_r_ref: &'static QueryServerReadV1,
// Store the token management parts.
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) origin: Url,
pub(crate) domain: String,
@ -211,7 +211,15 @@ pub async fn create_https_server(
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)
// Should be impossible!
@ -224,7 +232,7 @@ pub async fn create_https_server(
qe_w_ref,
qe_r_ref,
jws_signer,
trust_x_forward_for,
trust_x_forward_for_ips,
csp_header,
origin,
domain: config.domain.clone(),
@ -321,35 +329,41 @@ pub async fn create_https_server(
info!("Starting the web server...");
match maybe_tls_acceptor {
Some(tls_acceptor) => {
let listener = match TcpListener::bind(addr).await {
Ok(l) => l,
Err(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,
)))
let listener = match TcpListener::bind(addr).await {
Ok(l) => l,
Err(err) => {
error!(?err, "Failed to bind tcp listener");
return Err(());
}
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,
listener: TcpListener,
app: IntoMakeServiceWithConnectInfo<Router, ClientConnInfo>,
mut rx: broadcast::Receiver<CoreAction>,
server_message_tx: broadcast::Sender<CoreAction>,
mut tls_acceptor_reload_rx: mpsc::Receiver<SslAcceptor>,
trusted_proxy_v2_ips: Option<Arc<HashSet<IpAddr>>>,
) {
pin_mut!(listener);
@ -365,7 +379,7 @@ async fn server_loop(
Ok((stream, addr)) => {
let tls_acceptor = tls_acceptor.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) => {
error!("Web server exited with {:?}", err);
@ -386,24 +400,33 @@ async fn server_loop(
info!("Stopped {}", super::TaskName::HttpsServer);
}
async fn server_loop_plaintext(
addr: SocketAddr,
async fn server_plaintext_loop(
listener: TcpListener,
app: IntoMakeServiceWithConnectInfo<Router, ClientConnInfo>,
mut rx: broadcast::Receiver<CoreAction>,
trusted_proxy_v2_ips: Option<Arc<HashSet<IpAddr>>>,
) {
let listener = axum_server::bind(addr).serve(app);
pin_mut!(listener);
loop {
tokio::select! {
Ok(action) = rx.recv() => {
match action {
CoreAction::Shutdown =>
break,
CoreAction::Shutdown => 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.
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,
stream: TcpStream,
mut app: IntoMakeServiceWithConnectInfo<Router, ClientConnInfo>,
addr: SocketAddr,
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 ssl = Ssl::new(acceptor.context()).map_err(|e| {
error!("Failed to create TLS context: {:?}", e);
std::io::Error::from(ErrorKind::ConnectionAborted)
@ -459,42 +509,17 @@ pub(crate) async fn handle_conn(
None
};
let client_conn_info = ClientConnInfo { addr, client_cert };
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)
})?;
let client_conn_info = ClientConnInfo {
connection_addr,
client_addr,
client_cert,
};
// Hyper has its own `AsyncRead` and `AsyncWrite` traits and doesn't use tokio.
// `TokioIo` converts between them.
let stream = TokioIo::new(tls_stream);
// 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)
})
process_client_hyper(stream, app, client_conn_info).await
}
Err(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 futures_util::sink::SinkExt;
use futures_util::stream::StreamExt;
use haproxy_protocol::{ProxyHdrV2, RemoteAddress};
use hashbrown::HashSet;
use kanidmd_lib::idm::ldap::{LdapBoundToken, LdapResponseState};
use kanidmd_lib::prelude::*;
use ldap3_proto::proto::LdapMsg;
use ldap3_proto::LdapCodec;
use openssl::ssl::{Ssl, SslAcceptor};
use std::net;
use std::net::{IpAddr, SocketAddr};
use std::pin::Pin;
use std::str::FromStr;
use std::sync::Arc;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::broadcast;
@ -33,7 +36,7 @@ impl LdapSession {
#[instrument(name = "ldap-request", skip(client_address, qe_r_ref))]
async fn client_process_msg(
uat: Option<LdapBoundToken>,
client_address: net::SocketAddr,
client_address: SocketAddr,
protomsg: LdapMsg,
qe_r_ref: &'static QueryServerReadV1,
) -> Option<LdapResponseState> {
@ -50,7 +53,8 @@ async fn client_process_msg(
async fn client_process<STREAM>(
stream: STREAM,
client_address: net::SocketAddr,
client_address: SocketAddr,
connection_address: SocketAddr,
qe_r_ref: &'static QueryServerReadV1,
) where
STREAM: AsyncRead + AsyncWrite,
@ -67,6 +71,8 @@ async fn client_process<STREAM>(
let uat = session.uat.clone();
let caddr = client_address;
debug!(?client_address, ?connection_address);
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
// 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(
tcpstream: TcpStream,
stream: TcpStream,
tls_acceptor: SslAcceptor,
client_socket_addr: net::SocketAddr,
connection_addr: SocketAddr,
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
// From the parameters we need to create an SslContext.
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,
Err(err) => {
error!(?err, %client_socket_addr, "LDAP TLS setup error");
error!(?err, %client_addr, %connection_addr, "LDAP TLS setup error");
return;
}
};
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;
};
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]
@ -143,6 +186,7 @@ async fn ldap_tls_acceptor(
qe_r_ref: &'static QueryServerReadV1,
mut rx: broadcast::Receiver<CoreAction>,
mut tls_acceptor_reload_rx: mpsc::Receiver<SslAcceptor>,
trusted_proxy_v2_ips: Option<Arc<HashSet<IpAddr>>>,
) {
loop {
tokio::select! {
@ -155,7 +199,7 @@ async fn ldap_tls_acceptor(
match accept_result {
Ok((tcpstream, client_socket_addr)) => {
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) => {
warn!(?err, "LDAP acceptor error, continuing");
@ -187,7 +231,7 @@ async fn ldap_plaintext_acceptor(
accept_result = listener.accept() => {
match accept_result {
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) => {
error!("LDAP acceptor error, continuing -> {:?}", e);
@ -205,6 +249,7 @@ pub(crate) async fn create_ldap_server(
qe_r_ref: &'static QueryServerReadV1,
rx: broadcast::Receiver<CoreAction>,
tls_acceptor_reload_rx: mpsc::Receiver<SslAcceptor>,
trusted_proxy_v2_ips: Option<HashSet<IpAddr>>,
) -> Result<tokio::task::JoinHandle<()>, ()> {
if address.starts_with(":::") {
// 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);
};
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);
})?;
@ -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 {
Some(ssl_acceptor) => {
info!("Starting LDAPS interface ldaps://{} ...", address);
@ -233,6 +280,7 @@ pub(crate) async fn create_ldap_server(
qe_r_ref,
rx,
tls_acceptor_reload_rx,
trusted_proxy_v2_ips,
))
}
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,
broadcast_tx.subscribe(),
ldap_tls_acceptor_reload_rx,
config.ldap_client_address_info.trusted_proxy_v2(),
)
.await?;
Some(h)

View file

@ -2417,7 +2417,7 @@ mod tests {
let lims = Limits::unlimited();
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.
pub fn generate_secure(step: u64) -> Self {
let mut rng = rand::thread_rng();
let secret: Vec<u8> = (0..SECRET_SIZE_BYTES).map(|_| rng.gen()).collect();
let mut rng = rand::rng();
let secret: Vec<u8> = (0..SECRET_SIZE_BYTES).map(|_| rng.random()).collect();
let algo = TotpAlgo::Sha256;
let digits = TotpDigits::Six;
Totp {

View file

@ -17,6 +17,7 @@ use webauthn_rs::prelude::{
AttestedPasskey as AttestedPasskeyV4, AttestedPasskeyRegistration, CreationChallengeResponse,
Passkey as PasskeyV4, PasskeyRegistration, RegisterPublicKeyCredential, WebauthnError,
};
use zxcvbn::{zxcvbn, Score};
use crate::credential::totp::{Totp, TOTP_DEFAULT_STEP};
use crate::credential::{BackupCodes, Credential};
@ -1663,23 +1664,14 @@ impl IdmServerCredUpdateTransaction<'_> {
}
// does the password pass zxcvbn?
let entropy = zxcvbn::zxcvbn(cleartext, related_inputs).map_err(|e| {
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,
])
})?;
let entropy = zxcvbn(cleartext, related_inputs);
// 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:
// https://docs.rs/zxcvbn/2.0.0/zxcvbn/struct.Entropy.html
let feedback: zxcvbn::feedback::Feedback = entropy
.feedback()
.as_ref()
.ok_or(OperationError::InvalidState)
.cloned()
.map_err(|e| {
@ -3405,7 +3397,7 @@ mod tests {
assert!(
matches!(
c_status.mfaregstate,
MfaRegStateStatus::TotpNameTryAgain(ref val) if val == ""
MfaRegStateStatus::TotpNameTryAgain(ref val) if val.is_empty()
),
"{:?}",
c_status.mfaregstate

View file

@ -1209,7 +1209,7 @@ mod tests {
// Searching a malformed spn shouldn't cause the query to fail
let sr = SearchRequest {
msgid: 1,
base: format!("dc=example,dc=com"),
base: "dc=example,dc=com".to_string(),
scope: LdapSearchScope::Subtree,
filter: LdapFilter::Or(vec![
LdapFilter::Equality(Attribute::Name.to_string(), usr_name.to_string()),
@ -1232,7 +1232,7 @@ mod tests {
let sr = SearchRequest {
msgid: 1,
base: format!("dc=example,dc=com"),
base: "dc=example,dc=com".to_string(),
scope: LdapSearchScope::Subtree,
filter: LdapFilter::And(vec![
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))]
#[allow(dead_code)]
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];
// doing it here because of feature-shenanigans.
use rand::Rng;
if let Err(err) = rng.try_fill(&mut result) {
if let Err(err) = rng.try_fill_bytes(&mut result) {
error!("Failed to generate device code! {:?}", err);
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.
fn gen_user_code() -> (String, u32) {
use rand::Rng;
let mut rng = rand::thread_rng();
let num: u32 = rng.gen_range(0..=999999999);
let mut rng = rand::rng();
let num: u32 = rng.random_range(0..=999999999);
let result = format!("{:09}", num);
(
format!("{}-{}-{}", &result[0..3], &result[3..6], &result[6..9]),
@ -3100,6 +3101,7 @@ mod tests {
$code_challenge:expr,
$scope:expr
) => {{
#[allow(clippy::unnecessary_to_owned)]
let scope: BTreeSet<String> = $scope.split(" ").map(|s| s.to_string()).collect();
let auth_req = AuthorisationRequest {
@ -7312,10 +7314,7 @@ mod tests {
&Url::parse(example_is_not_local)
.expect("Failed to parse example.com as a host?")
.host()
.expect(&format!(
"Couldn't get a host from {}",
example_is_not_local
))
.unwrap_or_else(|| panic!("Couldn't get a host from {}", example_is_not_local))
));
let test_urls = [

View file

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

View file

@ -217,9 +217,9 @@ impl EntryChangeState {
}
#[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 {
State::Live { at: _, changes } => changes.get(&attr),
State::Live { at: _, changes } => changes.get(attr),
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 e2_cs = e2.get_changestate();
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");
drop(server_a_txn);

View file

@ -186,7 +186,7 @@ mod tests {
match desc {
ScimValueKanidm::String(gdesc) if gdesc == "Group Description" => {}
_ => assert!(false),
_ => unreachable!("Expected a string"),
};
// null removes attr
@ -201,7 +201,7 @@ mod tests {
.expect("Failed to resolve data type");
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
let put = ScimEntryPutKanidm {
@ -234,7 +234,7 @@ mod tests {
value: "extra_1@example.com".to_string(),
}));
}
_ => assert!(false),
_ => unreachable!("Expected 1 member"),
};
// set many
@ -285,7 +285,7 @@ mod tests {
value: "extra_3@example.com".to_string(),
}));
}
_ => assert!(false),
_ => unreachable!("Expected 3 members"),
};
// set many with a removal
@ -333,7 +333,7 @@ mod tests {
value: "extra_2@example.com".to_string(),
}));
}
_ => assert!(false),
_ => unreachable!("Expected 2 members"),
};
// empty set removes attr
@ -348,6 +348,6 @@ mod tests {
.expect("Failed to resolve data type");
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 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 {
fn default() -> Self {
TestConfiguration {
domain_level: DOMAIN_TGT_LEVEL,
ignore_this_field: false,
}
}
}

View file

@ -2,8 +2,8 @@
use crate::prelude::*;
use hashbrown::HashSet;
use rand::distributions::{Distribution, Uniform};
use rand::{thread_rng, Rng};
use rand::distr::{Distribution, Uniform};
use rand::{rng, Rng};
use std::ops::Range;
#[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 {
thread_rng()
rng()
.sample_iter(&DistinctAlpha)
.take(len as usize)
.collect::<String>()
@ -52,7 +52,7 @@ pub fn backup_code_from_random() -> HashSet<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.
// this leads us to 4 groups of 5 to create 55^20
let mut trng = thread_rng();
let mut trng = rng();
format!(
"{}-{}-{}-{}",
(&mut trng)
@ -80,8 +80,9 @@ impl Distribution<char> for DistinctAlpha {
const GEN_ASCII_STR_CHARSET: &[u8] = b"ABCDEFGHJKLMNPQRSTUVWXYZ\
abcdefghjkpqrstuvwxyz\
0123456789";
let range = Uniform::new(0, RANGE);
// TODO: this needs to handle the error, maybe?
#[allow(clippy::expect_used)]
let range = Uniform::new(0, RANGE).expect("Failed to get a uniform range");
let n = range.sample(rng);
GEN_ASCII_STR_CHARSET[n as usize] as char

View file

@ -683,10 +683,10 @@ mod tests {
"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.
crate::valueset::scim_json_put_reflexive::<ValueSetEmailAddress>(vs, &[])
crate::valueset::scim_json_put_reflexive::<ValueSetEmailAddress>(&vs, &[])
}
#[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.
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]
fn test_scim_boolean() {
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.
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);
// 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 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]
fn test_scim_credential_type() {
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.
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 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.
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() {
let vs: ValueSet =
ValueSetHexString::new("D68475C760A7A0F6A924C28F095573A967F600D6".to_string());
crate::valueset::scim_json_reflexive(
vs.clone(),
r#""D68475C760A7A0F6A924C28F095573A967F600D6""#,
);
crate::valueset::scim_json_reflexive(&vs, r#""D68475C760A7A0F6A924C28F095573A967F600D6""#);
}
}

View file

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

View file

@ -1,6 +1,7 @@
#![allow(dead_code)]
use crate::valueset::ScimResolveStatus;
use std::fmt::Display;
use std::io::Cursor;
use crate::be::dbvalue::DbValueImage;
use crate::prelude::*;
@ -37,8 +38,8 @@ pub trait ImageValueThings {
/// A sha256 of the filename/type/contents
fn hash_imagevalue(&self) -> String;
fn get_limits(&self) -> image::io::Limits {
let mut limits = image::io::Limits::default();
fn get_limits(&self) -> image::Limits {
let mut limits = image::Limits::default();
limits.max_image_height = Some(MAX_IMAGE_HEIGHT);
limits.max_image_width = Some(MAX_IMAGE_WIDTH);
limits
@ -148,7 +149,7 @@ impl ImageValueThings for ImageValue {
/// validate the GIF file contents, and that it's actually a GIF
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(
"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(
"Failed to parse WebP file".to_string(),
));
@ -532,7 +533,7 @@ mod tests {
"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]
fn test_scim_iname() {
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.
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]
fn test_scim_index() {
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.
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]
fn test_scim_iutf8() {
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.
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\"}"
]
"#;
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.
// 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)]
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 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)]
pub(crate) fn scim_json_reflexive_unresolved(
write_txn: &mut QueryServerWriteTransaction,
vs: ValueSet,
vs: &ValueSet,
data: &str,
) {
let scim_int_value = vs.to_scim_value().unwrap().assume_unresolved();
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);
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);
}
#[cfg(test)]
pub(crate) fn scim_json_put_reflexive<T: ValueSetScimPut>(
expect_vs: ValueSet,
expect_vs: &ValueSet,
additional_tests: &[(JsonValue, ValueSet)],
) {
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();
// Check that we can turn back into a vs from the generic version.
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 (jv, expect_vs) in additional_tests {
@ -1053,7 +1055,7 @@ pub(crate) fn scim_json_put_reflexive<T: ValueSetScimPut>(
#[cfg(test)]
pub(crate) fn scim_json_put_reflexive_unresolved<T: ValueSetScimPut>(
write_txn: &mut QueryServerWriteTransaction,
expect_vs: ValueSet,
expect_vs: &ValueSet,
additional_tests: &[(JsonValue, ValueSet)],
) {
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.
let vs_inter = T::from_scim_json_put(generic).unwrap().assume_unresolved();
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 (jv, expect_vs) in additional_tests {

View file

@ -189,12 +189,9 @@ mod tests {
fn test_scim_nsuniqueid() {
let vs: ValueSet =
ValueSetNsUniqueId::new("3a163ca0-47624620-a18806b7-50c84c86".to_string());
crate::valueset::scim_json_reflexive(
vs.clone(),
r#""3a163ca0-47624620-a18806b7-50c84c86""#,
);
crate::valueset::scim_json_reflexive(&vs, r#""3a163ca0-47624620-a18806b7-50c84c86""#);
// 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() {
let vs: ValueSet = ValueSetOauthScope::new("fully_sick_scope_m8".to_string());
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.
crate::valueset::scim_json_put_reflexive::<ValueSetOauthScope>(vs, &[])
crate::valueset::scim_json_put_reflexive::<ValueSetOauthScope>(&vs, &[])
}
#[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.
crate::valueset::scim_json_put_reflexive_unresolved::<ValueSetOauthScopeMap>(
&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.
crate::valueset::scim_json_put_reflexive_unresolved::<ValueSetOauthClaimMap>(
&mut write_txn,
vs,
&vs,
&[],
);

View file

@ -205,6 +205,6 @@ mod tests {
#[test]
fn test_scim_restricted() {
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]
@ -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]
fn test_scim_spn() {
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.
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]
fn test_scim_syntax() {
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.
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]
fn test_scim_uihint() {
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.
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]
fn test_scim_uint32() {
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.
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() {
let u = Url::parse("https://idm.example.com").unwrap();
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.
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() {
let vs: ValueSet = ValueSetUtf8::new("Test".to_string());
// 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.
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""#;
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.
crate::valueset::scim_json_put_reflexive::<ValueSetUuid>(vs, &[])
crate::valueset::scim_json_put_reflexive::<ValueSetUuid>(&vs, &[])
}
#[qs_test]
@ -449,12 +449,12 @@ mod tests {
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.
crate::valueset::scim_json_put_reflexive_unresolved::<ValueSetRefer>(
&mut write_txn,
vs,
&vs,
&[],
);

View file

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

View file

@ -44,7 +44,7 @@ pub fn cli_kanidm(_input: TokenStream) -> TokenStream {
.run()
.unwrap();
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
}

View file

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

View file

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

View file

@ -10,7 +10,7 @@ async fn test_v1_group_id_patch(rsclient: &KanidmClient) {
.await;
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"]}});
@ -31,7 +31,7 @@ async fn test_v1_group_id_attr_post(rsclient: &KanidmClient) {
.await;
assert!(res.is_ok());
create_user(&rsclient, "foo", "foogroup").await;
create_user(rsclient, "foo", "foogroup").await;
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]
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
setup_server(&rsclient).await;
create_user(&rsclient, USER_A_NAME).await;
setup_server(rsclient).await;
create_user(rsclient, USER_A_NAME).await;
let _ = rsclient.logout().await;
let res = rsclient
.idm_person_identify_user(USER_A_NAME, IdentifyUserRequest::Start)
@ -47,11 +47,11 @@ async fn test_not_authenticated(rsclient: &KanidmClient) {
#[kanidmd_testkit::test]
async fn test_non_existing_user_id(rsclient: &KanidmClient) {
setup_server(&rsclient).await;
create_user(&rsclient, USER_A_NAME).await;
create_user(&rsclient, USER_B_NAME).await;
setup_server(rsclient).await;
create_user(rsclient, USER_A_NAME).await;
create_user(rsclient, USER_B_NAME).await;
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
.idm_person_identify_user(non_existing_user, IdentifyUserRequest::Start)
.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}`
#[kanidmd_testkit::test]
async fn test_start_response_identity_verification_available(rsclient: &KanidmClient) {
setup_server(&rsclient).await;
create_user(&rsclient, USER_A_NAME).await;
login_with_user(&rsclient, USER_A_NAME).await;
setup_server(rsclient).await;
create_user(rsclient, USER_A_NAME).await;
login_with_user(rsclient, USER_A_NAME).await;
let response = rsclient
.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
#[kanidmd_testkit::test]
async fn test_start_response_wait_for_code_or_provide_code(rsclient: &KanidmClient) {
setup_server(&rsclient).await;
let user_a_uuid = create_user(&rsclient, USER_A_NAME).await;
let user_b_uuid = create_user(&rsclient, USER_B_NAME).await;
login_with_user(&rsclient, USER_A_NAME).await;
setup_server(rsclient).await;
let user_a_uuid = create_user(rsclient, USER_A_NAME).await;
let user_b_uuid = create_user(rsclient, USER_B_NAME).await;
login_with_user(rsclient, USER_A_NAME).await;
let response = rsclient
.idm_person_identify_user(USER_B_NAME, IdentifyUserRequest::Start)
.await;
@ -130,10 +130,10 @@ async fn test_start_response_wait_for_code_or_provide_code(rsclient: &KanidmClie
#[kanidmd_testkit::test]
async fn test_provide_code_response_code_failure_or_provide_code(rsclient: &KanidmClient) {
setup_server(&rsclient).await;
let user_a_uuid = create_user(&rsclient, USER_A_NAME).await;
let user_b_uuid = create_user(&rsclient, USER_B_NAME).await;
login_with_user(&rsclient, USER_A_NAME).await;
setup_server(rsclient).await;
let user_a_uuid = create_user(rsclient, USER_A_NAME).await;
let user_b_uuid = create_user(rsclient, USER_B_NAME).await;
login_with_user(rsclient, USER_A_NAME).await;
let response = rsclient
.idm_person_identify_user(
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
#[kanidmd_testkit::test]
async fn test_full_identification_flow(rsclient: &KanidmClient) {
setup_server(&rsclient).await;
let user_a_uuid = create_user(&rsclient, USER_A_NAME).await;
let user_b_uuid = create_user(&rsclient, USER_B_NAME).await;
setup_server(rsclient).await;
let user_a_uuid = create_user(rsclient, USER_A_NAME).await;
let user_b_uuid = create_user(rsclient, USER_B_NAME).await;
//user A session
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
let valid_user_b_client = valid_user_a_client.new_session().unwrap();
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 kanidmd_testkit::*;
use std::time::Duration;
login_put_admin_idm_admins(&rsclient).await;
login_put_admin_idm_admins(rsclient).await;
create_user_with_all_attrs(
&rsclient,
rsclient,
NOT_ADMIN_TEST_USERNAME,
Some(NOT_ADMIN_TEST_PASSWORD),
)
@ -89,7 +89,7 @@ async fn test_webdriver_user_login(rsclient: &KanidmClient) {
handle_error!(
c,
c.goto(&rsclient.get_url().to_string()).await,
c.goto(rsclient.get_url().to_string()).await,
"Couldn't get URL"
);
@ -207,7 +207,7 @@ async fn test_webdriver_user_login(rsclient: &KanidmClient) {
#[kanidmd_testkit::test]
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");
@ -220,7 +220,7 @@ async fn test_domain_reset_token_key(rsclient: &KanidmClient) {
#[kanidmd_testkit::test]
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
.idm_domain_set_ldap_basedn("dc=krabsarekool,dc=example,dc=com")
.await
@ -233,7 +233,7 @@ async fn test_idm_domain_set_ldap_basedn(rsclient: &KanidmClient) {
#[kanidmd_testkit::test]
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
.idm_domain_set_ldap_max_queryable_attrs(20)
.await
@ -247,7 +247,7 @@ async fn test_idm_domain_set_ldap_max_queryable_attrs(rsclient: &KanidmClient) {
#[kanidmd_testkit::test]
/// Checks that a built-in group idm_all_persons has the "builtin" class as expected.
async fn test_all_persons_has_builtin_class(rsclient: &KanidmClient) {
login_put_admin_idm_admins(&rsclient).await;
login_put_admin_idm_admins(rsclient).await;
let res = rsclient
.idm_group_get("idm_all_persons")
.await
@ -306,7 +306,7 @@ async fn test_all_persons_has_builtin_class(rsclient: &KanidmClient) {
/// Testing the CLI doing things.
async fn test_integration_with_assert_cmd(rsclient: KanidmClient) {
// setup the admin things
login_put_admin_idm_admins(&rsclient).await;
login_put_admin_idm_admins(rsclient).await;
rsclient
.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());
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
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(
&token_cache_path,
&rsclient,
rsclient,
&[
"service-account",
"create",
@ -355,13 +355,13 @@ async fn test_integration_with_assert_cmd(rsclient: KanidmClient) {
test_cmd_admin(
&token_cache_path,
&rsclient,
rsclient,
&format!("service-account get -D admin {}", NOT_ADMIN_TEST_USERNAME),
);
// updating the display name
test_cmd_admin(
&token_cache_path,
&rsclient,
rsclient,
&format!(
"service-account update -D admin {} --displayname cheeseballs",
NOT_ADMIN_TEST_USERNAME
@ -370,7 +370,7 @@ async fn test_integration_with_assert_cmd(rsclient: KanidmClient) {
// updating the email
test_cmd_admin(
&token_cache_path,
&rsclient,
rsclient,
&format!(
"service-account update -D admin {} --mail foo@bar.com",
NOT_ADMIN_TEST_USERNAME
@ -380,7 +380,7 @@ async fn test_integration_with_assert_cmd(rsclient: KanidmClient) {
// checking the email was changed
let sad = test_cmd_admin(
&token_cache_path,
&rsclient,
rsclient,
&format!(
"service-account get -D admin -o json {}",
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 group;
mod http_manifest;
mod https_extractors;
mod https_middleware;
mod identity_verification_tests;
mod integration;
mod ip_addr_extractors;
mod ldap_basic;
mod mtls_test;
mod oauth2_test;

View file

@ -10,7 +10,7 @@ async fn test_v1_person_id_patch(rsclient: &KanidmClient) {
.await;
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"]}});
@ -31,7 +31,7 @@ async fn test_v1_person_id_ssh_pubkeys_post(rsclient: &KanidmClient) {
.await;
assert!(res.is_ok());
create_user(&rsclient, "foo", "foogroup").await;
create_user(rsclient, "foo", "foogroup").await;
let post_body = serde_json::json!([
"ssh-key-tag-goes-here",

View file

@ -1366,7 +1366,7 @@ async fn setup_demo_account_password(
#[kanidmd_testkit::test]
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
.auth_passkey_begin("demo_account")
@ -1690,7 +1690,7 @@ async fn test_server_user_auth_token_lifecycle(rsclient: &KanidmClient) {
#[kanidmd_testkit::test]
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
.auth_passkey_begin("demo_account")
@ -1869,12 +1869,12 @@ async fn start_password_session(
#[kanidmd_testkit::test]
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
.expect("Failed to setup demo_account");
let uat = start_password_session(
&rsclient,
rsclient,
account_name.as_str(),
account_pass.as_str(),
false,
@ -1892,18 +1892,13 @@ async fn test_server_user_auth_unprivileged(rsclient: &KanidmClient) {
#[kanidmd_testkit::test]
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
.expect("Failed to setup demo_account");
let uat = start_password_session(
&rsclient,
account_name.as_str(),
account_pass.as_str(),
true,
)
.await
.expect("Failed to start session");
let uat = start_password_session(rsclient, account_name.as_str(), account_pass.as_str(), true)
.await
.expect("Failed to start session");
match 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) {
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_user(&rsclient, NOT_ADMIN_TEST_USERNAME, NAME_IDM_ADMINS).await;
login_account(&rsclient, "group_manager").await;
create_user(rsclient, NOT_ADMIN_TEST_USERNAME, NAME_IDM_ADMINS).await;
login_account(rsclient, "group_manager").await;
let response = rsclient
.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"));
}
login_put_admin_idm_admins(&rsclient).await;
login_put_admin_idm_admins(rsclient).await;
rsclient
.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::io::Read;
use tokio::task;
use zxcvbn::Score;
const CHUNK_SIZE: usize = 1000;
@ -67,10 +68,6 @@ impl PwBadlistOpt {
info!("Have {} unique passwords to process", pwset.len());
// 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
.chunks(CHUNK_SIZE)
.map(|chunk| chunk.to_vec())
@ -82,18 +79,7 @@ impl PwBadlistOpt {
if v.len() < 10 {
return false;
}
match zxcvbn::zxcvbn(v.as_str(), &[]) {
Ok(r) => r.score() >= 4,
Err(e) => {
error!(
"zxcvbn unable to process '{}' - {:?}",
v.as_str(),
e
);
error!("adding to badlist anyway ...");
true
}
}
zxcvbn::zxcvbn(v.as_str(), &[]).score() >= Score::Four
})
.map(|s| s.to_string())
.collect::<Vec<_>>();

View file

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

View file

@ -4,8 +4,9 @@ use crate::model::ActorRole;
use crate::profile::Profile;
use crate::state::{Credential, Flag, Group, GroupName, Person, PreflightState, State};
use hashbrown::HashMap;
use rand::distributions::{Alphanumeric, DistString, Uniform};
use rand::seq::{index, SliceRandom};
use rand::distr::{Alphanumeric, SampleString, Uniform};
use rand::seq::{index, IndexedRandom};
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
@ -171,7 +172,8 @@ pub async fn populate(_client: &KanidmOrcaClient, profile: Profile) -> Result<St
let baseline = persons.len() / 3;
let inverse = persons.len() - baseline;
// 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)
}
};

View file

@ -27,7 +27,7 @@ impl ActorBasic {
pub fn new(mut cha_rng: ChaCha8Rng, warmup_time_ms: u64) -> Self {
let max_backoff_time_in_ms = 2 * warmup_time_ms / 3;
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 {
state: State::Unauthenticated,
randomised_backoff_time,

View file

@ -76,7 +76,7 @@ impl ActorLatencyMeasurer {
let max_backoff_time_in_ms = 2 * warmup_time_ms / 3;
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 {
state: State::Unauthenticated,
randomised_backoff_time,

View file

@ -25,7 +25,7 @@ impl ActorReader {
pub fn new(mut cha_rng: ChaCha8Rng, warmup_time_ms: u64) -> Self {
let max_backoff_time_in_ms = warmup_time_ms - 1000;
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 {
state: State::Unauthenticated,
randomised_backoff_time,

View file

@ -26,7 +26,7 @@ impl ActorWriter {
pub fn new(mut cha_rng: ChaCha8Rng, warmup_time_ms: u64) -> Self {
let max_backoff_time_in_ms = 2 * warmup_time_ms / 3;
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 {
state: State::Unauthenticated,
randomised_backoff_time,

View file

@ -1,6 +1,6 @@
use crate::error::Error;
use crate::state::{GroupName, Model};
use rand::{thread_rng, Rng};
use rand::{rng, Rng};
use serde::de::{value, IntoDeserializer};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
@ -200,8 +200,8 @@ impl ProfileBuilder {
} = self;
let seed: u64 = seed.unwrap_or_else(|| {
let mut rng = thread_rng();
rng.gen()
let mut rng = rng();
rng.random()
});
//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<_>, _>>()?;
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);
//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)
.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()
.expect("Can't read examples dir!")
{

View file

@ -71,32 +71,32 @@ impl RequestOptions {
fn nss_fallback_unavail() {
let req_opt = RequestOptions::fallback_unavail();
let Response::Unavail = core::get_all_user_entries(req_opt) else {
unreachable!();
panic!("unrecoverable");
};
let req_opt = RequestOptions::fallback_unavail();
let Response::Unavail = core::get_user_entry_by_uid(0, req_opt) else {
unreachable!();
panic!("unrecoverable");
};
let req_opt = RequestOptions::fallback_unavail();
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 Response::Unavail = core::get_all_group_entries(req_opt) else {
unreachable!();
panic!("unrecoverable");
};
let req_opt = RequestOptions::fallback_unavail();
let Response::Unavail = core::get_group_entry_by_gid(0, req_opt) else {
unreachable!();
panic!("unrecoverable");
};
let req_opt = RequestOptions::fallback_unavail();
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 Response::Success(users) = core::get_all_user_entries(req_opt) else {
unreachable!();
panic!("Failed to get all user entries");
};
assert_eq!(users.len(), 3);
@ -129,7 +129,7 @@ fn nss_fallback_all_user_entries() {
fn nss_fallback_user_entry_by_uid() {
let req_opt = RequestOptions::fallback_fixture();
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");
@ -139,7 +139,7 @@ fn nss_fallback_user_entry_by_uid() {
let req_opt = RequestOptions::fallback_fixture();
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");
@ -149,7 +149,7 @@ fn nss_fallback_user_entry_by_uid() {
let req_opt = RequestOptions::fallback_fixture();
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() {
let req_opt = RequestOptions::fallback_fixture();
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");
@ -167,7 +167,7 @@ fn nss_fallback_user_entry_by_name() {
let req_opt = RequestOptions::fallback_fixture();
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");
@ -177,7 +177,7 @@ fn nss_fallback_user_entry_by_name() {
let req_opt = RequestOptions::fallback_fixture();
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 Response::Success(groups) = core::get_all_group_entries(req_opt) else {
unreachable!();
panic!("Failed to get all group entries");
};
assert_eq!(groups.len(), 3);
@ -207,7 +207,7 @@ fn nss_fallback_all_group_entries() {
fn nss_fallback_group_entry_by_uid() {
let req_opt = RequestOptions::fallback_fixture();
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");
@ -216,7 +216,7 @@ fn nss_fallback_group_entry_by_uid() {
let req_opt = RequestOptions::fallback_fixture();
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");
@ -225,7 +225,7 @@ fn nss_fallback_group_entry_by_uid() {
let req_opt = RequestOptions::fallback_fixture();
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 Response::Success(group) = core::get_group_entry_by_name("root".to_string(), req_opt)
else {
unreachable!();
panic!("Failed to get group entry by name");
};
assert_eq!(group.name, "root");
@ -244,7 +244,7 @@ fn nss_fallback_group_entry_by_name() {
let req_opt = RequestOptions::fallback_fixture();
let Response::Success(group) = core::get_group_entry_by_name("ellie".to_string(), req_opt)
else {
unreachable!();
panic!("Failed to get group entry by name");
};
assert_eq!(group.name, "ellie");
@ -253,6 +253,6 @@ fn nss_fallback_group_entry_by_name() {
let req_opt = RequestOptions::fallback_fixture();
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.
fn message(&self, _prompt: &str) -> PamResult<()> {
let mut q = self.response_queue.lock().unwrap();
match q.pop_front() {
e => {
eprintln!("{:?}", e);
panic!("Invalid event transition");
}
}
let e = q.pop_front();
eprintln!("{:?}", e);
panic!("Invalid event transition message");
}
/// Display a device grant request to the user.
fn message_device_grant(&self, _data: &DeviceAuthorizationResponse) -> PamResult<()> {
let mut q = self.response_queue.lock().unwrap();
match q.pop_front() {
e => {
eprintln!("{:?}", e);
panic!("Invalid event transition");
}
}
let e = q.pop_front();
eprintln!("{:?}", e);
panic!("Invalid event transition message_device_grant");
}
/// 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>> {
let mut q = self.response_queue.lock().unwrap();
match q.pop_front() {
e => {
eprintln!("{:?}", e);
panic!("Invalid event transition");
}
}
let e = q.pop_front();
eprintln!("{:?}", e);
panic!("Invalid event transition prompt_for_pin");
}
fn prompt_for_mfacode(&self) -> PamResult<Option<String>> {
let mut q = self.response_queue.lock().unwrap();
match q.pop_front() {
e => {
eprintln!("{:?}", e);
panic!("Invalid event transition");
}
}
let mut q = self.response_queue.lock().expect("Failed to lock mutex");
let e = q.pop_front();
eprintln!("{:?}", e);
panic!("Invalid event transition prompt_for_mfacode");
}
}

View file

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