mirror of
https://github.com/kanidm/kanidm.git
synced 2025-05-25 02:13:55 +02:00
Compare commits
12 commits
0fe42f62bd
...
a598ca7e61
Author | SHA1 | Date | |
---|---|---|---|
|
a598ca7e61 | ||
|
c85054a389 | ||
|
76752642b4 | ||
|
742aa0aef7 | ||
|
4775f5ecd6 | ||
|
be4818e121 | ||
|
5201ec11e8 | ||
|
e97f4bc54c | ||
|
20433f5712 | ||
|
8424863969 | ||
|
9b7c542b0a | ||
|
d0cfc69a09 |
Cargo.lockCargo.toml
examples
libs
proto/src
pykanidm
server
core
lib/src
be
constants
credential
entry.rsevent.rsidm
migration_data/dl10
plugins
repl
server
testkit.rsutils.rsvalueset
testkit-macros/src
testkit
tools/cli/src/cli/system_config
837
Cargo.lock
generated
837
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
54
Cargo.toml
54
Cargo.toml
|
@ -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"
|
||||
|
|
|
@ -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/"
|
||||
|
|
|
@ -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/"
|
||||
|
|
19
libs/client/src/application.rs
Normal file
19
libs/client/src/application.rs
Normal file
|
@ -0,0 +1,19 @@
|
|||
use crate::{ClientError, KanidmClient};
|
||||
use kanidm_proto::scim_v1::client::{ScimEntryApplication, ScimEntryApplicationPost};
|
||||
|
||||
impl KanidmClient {
|
||||
/// Delete an application
|
||||
pub async fn idm_application_delete(&self, id: &str) -> Result<(), ClientError> {
|
||||
self.perform_delete_request(format!("/scim/v1/Application/{}", id).as_str())
|
||||
.await
|
||||
}
|
||||
|
||||
/// Create an application
|
||||
pub async fn idm_application_create(
|
||||
&self,
|
||||
application: &ScimEntryApplicationPost,
|
||||
) -> Result<ScimEntryApplication, ClientError> {
|
||||
self.perform_post_request("/scim/v1/Application", application)
|
||||
.await
|
||||
}
|
||||
}
|
|
@ -50,6 +50,7 @@ use webauthn_rs_proto::{
|
|||
PublicKeyCredential, RegisterPublicKeyCredential, RequestChallengeResponse,
|
||||
};
|
||||
|
||||
mod application;
|
||||
mod domain;
|
||||
mod group;
|
||||
mod oauth;
|
||||
|
|
|
@ -2,12 +2,10 @@ use crate::{ClientError, KanidmClient};
|
|||
use kanidm_proto::scim_v1::{ScimEntryGeneric, ScimEntryGetQuery, ScimSyncRequest, ScimSyncState};
|
||||
|
||||
impl KanidmClient {
|
||||
// TODO: testing for this
|
||||
pub async fn scim_v1_sync_status(&self) -> Result<ScimSyncState, ClientError> {
|
||||
self.perform_get_request("/scim/v1/Sync").await
|
||||
}
|
||||
|
||||
// TODO: testing for this
|
||||
pub async fn scim_v1_sync_update(
|
||||
&self,
|
||||
scim_sync_request: &ScimSyncRequest,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -32,6 +32,7 @@ pub enum Attribute {
|
|||
AcpTargetScope,
|
||||
ApiTokenSession,
|
||||
ApplicationPassword,
|
||||
ApplicationUrl,
|
||||
AttestedPasskeys,
|
||||
#[default]
|
||||
Attr,
|
||||
|
@ -267,6 +268,7 @@ impl Attribute {
|
|||
Attribute::AcpTargetScope => ATTR_ACP_TARGET_SCOPE,
|
||||
Attribute::ApiTokenSession => ATTR_API_TOKEN_SESSION,
|
||||
Attribute::ApplicationPassword => ATTR_APPLICATION_PASSWORD,
|
||||
Attribute::ApplicationUrl => ATTR_APPLICATION_URL,
|
||||
Attribute::AttestedPasskeys => ATTR_ATTESTED_PASSKEYS,
|
||||
Attribute::Attr => ATTR_ATTR,
|
||||
Attribute::AttributeName => ATTR_ATTRIBUTENAME,
|
||||
|
@ -454,6 +456,7 @@ impl Attribute {
|
|||
ATTR_ACP_TARGET_SCOPE => Attribute::AcpTargetScope,
|
||||
ATTR_API_TOKEN_SESSION => Attribute::ApiTokenSession,
|
||||
ATTR_APPLICATION_PASSWORD => Attribute::ApplicationPassword,
|
||||
ATTR_APPLICATION_URL => Attribute::ApplicationUrl,
|
||||
ATTR_ATTESTED_PASSKEYS => Attribute::AttestedPasskeys,
|
||||
ATTR_ATTR => Attribute::Attr,
|
||||
ATTR_ATTRIBUTENAME => Attribute::AttributeName,
|
||||
|
|
|
@ -72,6 +72,7 @@ pub const ATTR_ACP_SEARCH_ATTR: &str = "acp_search_attr";
|
|||
pub const ATTR_ACP_TARGET_SCOPE: &str = "acp_targetscope";
|
||||
pub const ATTR_API_TOKEN_SESSION: &str = "api_token_session";
|
||||
pub const ATTR_APPLICATION_PASSWORD: &str = "application_password";
|
||||
pub const ATTR_APPLICATION_URL: &str = "application_url";
|
||||
pub const ATTR_ATTESTED_PASSKEYS: &str = "attested_passkeys";
|
||||
pub const ATTR_ATTR: &str = "attr";
|
||||
pub const ATTR_ATTRIBUTENAME: &str = "attributename";
|
||||
|
|
|
@ -213,6 +213,8 @@ pub enum OperationError {
|
|||
SC0024SshPublicKeySyntaxInvalid,
|
||||
SC0025UiHintSyntaxInvalid,
|
||||
SC0026Utf8SyntaxInvalid,
|
||||
SC0027ClassSetInvalid,
|
||||
SC0028CreatedUuidsInvalid,
|
||||
// Migration
|
||||
MG0001InvalidReMigrationLevel,
|
||||
MG0002RaiseDomainLevelExceedsMaximum,
|
||||
|
@ -492,6 +494,8 @@ impl OperationError {
|
|||
Self::SC0024SshPublicKeySyntaxInvalid => Some("A SCIM Ssh Public Key contained invalid syntax".into()),
|
||||
Self::SC0025UiHintSyntaxInvalid => Some("A SCIM UiHint contained invalid syntax".into()),
|
||||
Self::SC0026Utf8SyntaxInvalid => Some("A SCIM Utf8 String Scope Map contained invalid syntax".into()),
|
||||
Self::SC0027ClassSetInvalid => Some("The internal set of class templates used in this create operation was invalid. THIS IS A BUG.".into()),
|
||||
Self::SC0028CreatedUuidsInvalid => Some("The internal create query did not return the set of created UUIDs. THIS IS A BUG".into()),
|
||||
|
||||
Self::UI0001ChallengeSerialisation => Some("The WebAuthn challenge was unable to be serialised.".into()),
|
||||
Self::UI0002InvalidState => Some("The credential update process returned an invalid state transition.".into()),
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
use super::ScimEntryGetQuery;
|
||||
use super::ScimOauth2ClaimMapJoinChar;
|
||||
use crate::attribute::{Attribute, SubAttribute};
|
||||
use scim_proto::ScimEntryHeader;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value as JsonValue;
|
||||
use serde_with::formats::PreferMany;
|
||||
|
@ -31,6 +32,18 @@ pub struct ScimReference {
|
|||
pub value: Option<String>,
|
||||
}
|
||||
|
||||
impl<T> From<T> for ScimReference
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
fn from(value: T) -> Self {
|
||||
ScimReference {
|
||||
uuid: None,
|
||||
value: Some(value.as_ref().to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type ScimReferences = Vec<ScimReference>;
|
||||
|
||||
#[serde_as]
|
||||
|
@ -79,6 +92,31 @@ pub struct ScimOAuth2ScopeMap {
|
|||
pub scopes: BTreeSet<String>,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct ScimEntryApplicationPost {
|
||||
pub name: String,
|
||||
pub displayname: String,
|
||||
pub linked_group: ScimReference,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct ScimEntryApplication {
|
||||
#[serde(flatten)]
|
||||
pub header: ScimEntryHeader,
|
||||
|
||||
pub name: String,
|
||||
pub displayname: String,
|
||||
|
||||
pub linked_group: Vec<super::ScimReference>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub attrs: BTreeMap<Attribute, JsonValue>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct ScimEntryPutKanidm {
|
||||
pub id: Uuid,
|
||||
|
@ -90,6 +128,13 @@ pub struct ScimEntryPutKanidm {
|
|||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct ScimStrings(#[serde_as(as = "OneOrMany<_, PreferMany>")] pub Vec<String>);
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Default)]
|
||||
pub struct ScimEntryPostGeneric {
|
||||
/// Create an attribute to contain the following value state.
|
||||
#[serde(flatten)]
|
||||
pub attrs: BTreeMap<Attribute, JsonValue>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Default)]
|
||||
pub struct ScimEntryPutGeneric {
|
||||
// id is only used to target the entry in question
|
||||
|
|
|
@ -18,13 +18,13 @@
|
|||
|
||||
use crate::attribute::Attribute;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::formats::CommaSeparator;
|
||||
use serde_with::{serde_as, skip_serializing_none, StringWithSeparator};
|
||||
use sshkey_attest::proto::PublicKey as SshPublicKey;
|
||||
use std::collections::BTreeMap;
|
||||
use std::ops::Not;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use serde_with::formats::CommaSeparator;
|
||||
use serde_with::{serde_as, skip_serializing_none, StringWithSeparator};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub use self::synch::*;
|
||||
pub use scim_proto::prelude::*;
|
||||
|
@ -86,6 +86,13 @@ pub struct ScimSshPublicKey {
|
|||
pub value: SshPublicKey,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ScimReference {
|
||||
pub uuid: Uuid,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone, ToSchema)]
|
||||
pub enum ScimOauth2ClaimMapJoinChar {
|
||||
#[serde(rename = ",", alias = "csv")]
|
||||
|
|
216
pykanidm/poetry.lock
generated
216
pykanidm/poetry.lock
generated
|
@ -14,93 +14,93 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
version = "3.11.16"
|
||||
version = "3.11.17"
|
||||
description = "Async http client/server framework (asyncio)"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "aiohttp-3.11.16-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb46bb0f24813e6cede6cc07b1961d4b04f331f7112a23b5e21f567da4ee50aa"},
|
||||
{file = "aiohttp-3.11.16-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:54eb3aead72a5c19fad07219acd882c1643a1027fbcdefac9b502c267242f955"},
|
||||
{file = "aiohttp-3.11.16-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:38bea84ee4fe24ebcc8edeb7b54bf20f06fd53ce4d2cc8b74344c5b9620597fd"},
|
||||
{file = "aiohttp-3.11.16-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0666afbe984f6933fe72cd1f1c3560d8c55880a0bdd728ad774006eb4241ecd"},
|
||||
{file = "aiohttp-3.11.16-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ba92a2d9ace559a0a14b03d87f47e021e4fa7681dc6970ebbc7b447c7d4b7cd"},
|
||||
{file = "aiohttp-3.11.16-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ad1d59fd7114e6a08c4814983bb498f391c699f3c78712770077518cae63ff7"},
|
||||
{file = "aiohttp-3.11.16-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b88a2bf26965f2015a771381624dd4b0839034b70d406dc74fd8be4cc053e3"},
|
||||
{file = "aiohttp-3.11.16-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:576f5ca28d1b3276026f7df3ec841ae460e0fc3aac2a47cbf72eabcfc0f102e1"},
|
||||
{file = "aiohttp-3.11.16-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a2a450bcce4931b295fc0848f384834c3f9b00edfc2150baafb4488c27953de6"},
|
||||
{file = "aiohttp-3.11.16-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:37dcee4906454ae377be5937ab2a66a9a88377b11dd7c072df7a7c142b63c37c"},
|
||||
{file = "aiohttp-3.11.16-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4d0c970c0d602b1017e2067ff3b7dac41c98fef4f7472ec2ea26fd8a4e8c2149"},
|
||||
{file = "aiohttp-3.11.16-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:004511d3413737700835e949433536a2fe95a7d0297edd911a1e9705c5b5ea43"},
|
||||
{file = "aiohttp-3.11.16-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:c15b2271c44da77ee9d822552201180779e5e942f3a71fb74e026bf6172ff287"},
|
||||
{file = "aiohttp-3.11.16-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ad9509ffb2396483ceacb1eee9134724443ee45b92141105a4645857244aecc8"},
|
||||
{file = "aiohttp-3.11.16-cp310-cp310-win32.whl", hash = "sha256:634d96869be6c4dc232fc503e03e40c42d32cfaa51712aee181e922e61d74814"},
|
||||
{file = "aiohttp-3.11.16-cp310-cp310-win_amd64.whl", hash = "sha256:938f756c2b9374bbcc262a37eea521d8a0e6458162f2a9c26329cc87fdf06534"},
|
||||
{file = "aiohttp-3.11.16-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8cb0688a8d81c63d716e867d59a9ccc389e97ac7037ebef904c2b89334407180"},
|
||||
{file = "aiohttp-3.11.16-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ad1fb47da60ae1ddfb316f0ff16d1f3b8e844d1a1e154641928ea0583d486ed"},
|
||||
{file = "aiohttp-3.11.16-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:df7db76400bf46ec6a0a73192b14c8295bdb9812053f4fe53f4e789f3ea66bbb"},
|
||||
{file = "aiohttp-3.11.16-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc3a145479a76ad0ed646434d09216d33d08eef0d8c9a11f5ae5cdc37caa3540"},
|
||||
{file = "aiohttp-3.11.16-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d007aa39a52d62373bd23428ba4a2546eed0e7643d7bf2e41ddcefd54519842c"},
|
||||
{file = "aiohttp-3.11.16-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6ddd90d9fb4b501c97a4458f1c1720e42432c26cb76d28177c5b5ad4e332601"},
|
||||
{file = "aiohttp-3.11.16-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a2f451849e6b39e5c226803dcacfa9c7133e9825dcefd2f4e837a2ec5a3bb98"},
|
||||
{file = "aiohttp-3.11.16-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8df6612df74409080575dca38a5237282865408016e65636a76a2eb9348c2567"},
|
||||
{file = "aiohttp-3.11.16-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:78e6e23b954644737e385befa0deb20233e2dfddf95dd11e9db752bdd2a294d3"},
|
||||
{file = "aiohttp-3.11.16-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:696ef00e8a1f0cec5e30640e64eca75d8e777933d1438f4facc9c0cdf288a810"},
|
||||
{file = "aiohttp-3.11.16-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e3538bc9fe1b902bef51372462e3d7c96fce2b566642512138a480b7adc9d508"},
|
||||
{file = "aiohttp-3.11.16-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3ab3367bb7f61ad18793fea2ef71f2d181c528c87948638366bf1de26e239183"},
|
||||
{file = "aiohttp-3.11.16-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:56a3443aca82abda0e07be2e1ecb76a050714faf2be84256dae291182ba59049"},
|
||||
{file = "aiohttp-3.11.16-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:61c721764e41af907c9d16b6daa05a458f066015abd35923051be8705108ed17"},
|
||||
{file = "aiohttp-3.11.16-cp311-cp311-win32.whl", hash = "sha256:3e061b09f6fa42997cf627307f220315e313ece74907d35776ec4373ed718b86"},
|
||||
{file = "aiohttp-3.11.16-cp311-cp311-win_amd64.whl", hash = "sha256:745f1ed5e2c687baefc3c5e7b4304e91bf3e2f32834d07baaee243e349624b24"},
|
||||
{file = "aiohttp-3.11.16-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:911a6e91d08bb2c72938bc17f0a2d97864c531536b7832abee6429d5296e5b27"},
|
||||
{file = "aiohttp-3.11.16-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac13b71761e49d5f9e4d05d33683bbafef753e876e8e5a7ef26e937dd766713"},
|
||||
{file = "aiohttp-3.11.16-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fd36c119c5d6551bce374fcb5c19269638f8d09862445f85a5a48596fd59f4bb"},
|
||||
{file = "aiohttp-3.11.16-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d489d9778522fbd0f8d6a5c6e48e3514f11be81cb0a5954bdda06f7e1594b321"},
|
||||
{file = "aiohttp-3.11.16-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69a2cbd61788d26f8f1e626e188044834f37f6ae3f937bd9f08b65fc9d7e514e"},
|
||||
{file = "aiohttp-3.11.16-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd464ba806e27ee24a91362ba3621bfc39dbbb8b79f2e1340201615197370f7c"},
|
||||
{file = "aiohttp-3.11.16-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ce63ae04719513dd2651202352a2beb9f67f55cb8490c40f056cea3c5c355ce"},
|
||||
{file = "aiohttp-3.11.16-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09b00dd520d88eac9d1768439a59ab3d145065c91a8fab97f900d1b5f802895e"},
|
||||
{file = "aiohttp-3.11.16-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7f6428fee52d2bcf96a8aa7b62095b190ee341ab0e6b1bcf50c615d7966fd45b"},
|
||||
{file = "aiohttp-3.11.16-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:13ceac2c5cdcc3f64b9015710221ddf81c900c5febc505dbd8f810e770011540"},
|
||||
{file = "aiohttp-3.11.16-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fadbb8f1d4140825069db3fedbbb843290fd5f5bc0a5dbd7eaf81d91bf1b003b"},
|
||||
{file = "aiohttp-3.11.16-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6a792ce34b999fbe04a7a71a90c74f10c57ae4c51f65461a411faa70e154154e"},
|
||||
{file = "aiohttp-3.11.16-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f4065145bf69de124accdd17ea5f4dc770da0a6a6e440c53f6e0a8c27b3e635c"},
|
||||
{file = "aiohttp-3.11.16-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fa73e8c2656a3653ae6c307b3f4e878a21f87859a9afab228280ddccd7369d71"},
|
||||
{file = "aiohttp-3.11.16-cp312-cp312-win32.whl", hash = "sha256:f244b8e541f414664889e2c87cac11a07b918cb4b540c36f7ada7bfa76571ea2"},
|
||||
{file = "aiohttp-3.11.16-cp312-cp312-win_amd64.whl", hash = "sha256:23a15727fbfccab973343b6d1b7181bfb0b4aa7ae280f36fd2f90f5476805682"},
|
||||
{file = "aiohttp-3.11.16-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a3814760a1a700f3cfd2f977249f1032301d0a12c92aba74605cfa6ce9f78489"},
|
||||
{file = "aiohttp-3.11.16-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9b751a6306f330801665ae69270a8a3993654a85569b3469662efaad6cf5cc50"},
|
||||
{file = "aiohttp-3.11.16-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ad497f38a0d6c329cb621774788583ee12321863cd4bd9feee1effd60f2ad133"},
|
||||
{file = "aiohttp-3.11.16-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca37057625693d097543bd88076ceebeb248291df9d6ca8481349efc0b05dcd0"},
|
||||
{file = "aiohttp-3.11.16-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5abcbba9f4b463a45c8ca8b7720891200658f6f46894f79517e6cd11f3405ca"},
|
||||
{file = "aiohttp-3.11.16-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f420bfe862fb357a6d76f2065447ef6f484bc489292ac91e29bc65d2d7a2c84d"},
|
||||
{file = "aiohttp-3.11.16-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58ede86453a6cf2d6ce40ef0ca15481677a66950e73b0a788917916f7e35a0bb"},
|
||||
{file = "aiohttp-3.11.16-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fdec0213244c39973674ca2a7f5435bf74369e7d4e104d6c7473c81c9bcc8c4"},
|
||||
{file = "aiohttp-3.11.16-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72b1b03fb4655c1960403c131740755ec19c5898c82abd3961c364c2afd59fe7"},
|
||||
{file = "aiohttp-3.11.16-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:780df0d837276276226a1ff803f8d0fa5f8996c479aeef52eb040179f3156cbd"},
|
||||
{file = "aiohttp-3.11.16-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ecdb8173e6c7aa09eee342ac62e193e6904923bd232e76b4157ac0bfa670609f"},
|
||||
{file = "aiohttp-3.11.16-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a6db7458ab89c7d80bc1f4e930cc9df6edee2200127cfa6f6e080cf619eddfbd"},
|
||||
{file = "aiohttp-3.11.16-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2540ddc83cc724b13d1838026f6a5ad178510953302a49e6d647f6e1de82bc34"},
|
||||
{file = "aiohttp-3.11.16-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3b4e6db8dc4879015b9955778cfb9881897339c8fab7b3676f8433f849425913"},
|
||||
{file = "aiohttp-3.11.16-cp313-cp313-win32.whl", hash = "sha256:493910ceb2764f792db4dc6e8e4b375dae1b08f72e18e8f10f18b34ca17d0979"},
|
||||
{file = "aiohttp-3.11.16-cp313-cp313-win_amd64.whl", hash = "sha256:42864e70a248f5f6a49fdaf417d9bc62d6e4d8ee9695b24c5916cb4bb666c802"},
|
||||
{file = "aiohttp-3.11.16-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bbcba75fe879ad6fd2e0d6a8d937f34a571f116a0e4db37df8079e738ea95c71"},
|
||||
{file = "aiohttp-3.11.16-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:87a6e922b2b2401e0b0cf6b976b97f11ec7f136bfed445e16384fbf6fd5e8602"},
|
||||
{file = "aiohttp-3.11.16-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccf10f16ab498d20e28bc2b5c1306e9c1512f2840f7b6a67000a517a4b37d5ee"},
|
||||
{file = "aiohttp-3.11.16-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb3d0cc5cdb926090748ea60172fa8a213cec728bd6c54eae18b96040fcd6227"},
|
||||
{file = "aiohttp-3.11.16-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d07502cc14ecd64f52b2a74ebbc106893d9a9717120057ea9ea1fd6568a747e7"},
|
||||
{file = "aiohttp-3.11.16-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:776c8e959a01e5e8321f1dec77964cb6101020a69d5a94cd3d34db6d555e01f7"},
|
||||
{file = "aiohttp-3.11.16-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0902e887b0e1d50424112f200eb9ae3dfed6c0d0a19fc60f633ae5a57c809656"},
|
||||
{file = "aiohttp-3.11.16-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e87fd812899aa78252866ae03a048e77bd11b80fb4878ce27c23cade239b42b2"},
|
||||
{file = "aiohttp-3.11.16-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0a950c2eb8ff17361abd8c85987fd6076d9f47d040ebffce67dce4993285e973"},
|
||||
{file = "aiohttp-3.11.16-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:c10d85e81d0b9ef87970ecbdbfaeec14a361a7fa947118817fcea8e45335fa46"},
|
||||
{file = "aiohttp-3.11.16-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:7951decace76a9271a1ef181b04aa77d3cc309a02a51d73826039003210bdc86"},
|
||||
{file = "aiohttp-3.11.16-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14461157d8426bcb40bd94deb0450a6fa16f05129f7da546090cebf8f3123b0f"},
|
||||
{file = "aiohttp-3.11.16-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9756d9b9d4547e091f99d554fbba0d2a920aab98caa82a8fb3d3d9bee3c9ae85"},
|
||||
{file = "aiohttp-3.11.16-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:87944bd16b7fe6160607f6a17808abd25f17f61ae1e26c47a491b970fb66d8cb"},
|
||||
{file = "aiohttp-3.11.16-cp39-cp39-win32.whl", hash = "sha256:92b7ee222e2b903e0a4b329a9943d432b3767f2d5029dbe4ca59fb75223bbe2e"},
|
||||
{file = "aiohttp-3.11.16-cp39-cp39-win_amd64.whl", hash = "sha256:17ae4664031aadfbcb34fd40ffd90976671fa0c0286e6c4113989f78bebab37a"},
|
||||
{file = "aiohttp-3.11.16.tar.gz", hash = "sha256:16f8a2c9538c14a557b4d309ed4d0a7c60f0253e8ed7b6c9a2859a7582f8b1b8"},
|
||||
{file = "aiohttp-3.11.17-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:67eaabe31454d68503ddc10d805944187787b4351600aedda1724f75f3f20b6c"},
|
||||
{file = "aiohttp-3.11.17-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c73371bb91660eb7971336883eb05ebf3e912a0a183f5029fe6200285dd858e"},
|
||||
{file = "aiohttp-3.11.17-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:78568d883e7cabf31f110a87bb02cee70e32bbb419eed974027c26ac6be18add"},
|
||||
{file = "aiohttp-3.11.17-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d5f480d3e35a139f0bd31d9037047cc18d6f362d1b06243b694a40f1a658ba8"},
|
||||
{file = "aiohttp-3.11.17-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:562b47fa14712e47ae7b6cf31998a4c888c35a904845e27f5ec2fc51401304e7"},
|
||||
{file = "aiohttp-3.11.17-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3875dd6571d2709697835cf5e4e7e0e1bf1d6e3aeec21b7766bbd50d9a97b8a3"},
|
||||
{file = "aiohttp-3.11.17-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d9130c4380bb02a308c79a25d9b82e642e21bea5ad453553f35f2d490ac47bd"},
|
||||
{file = "aiohttp-3.11.17-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:71ee4844e4680e69d2acb462073b147a4c2dd6813c30c61979399feddc63b322"},
|
||||
{file = "aiohttp-3.11.17-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e40eceff6ca85d68d07ebd1533f13727261d17b4bef7a518a6882b8448c83106"},
|
||||
{file = "aiohttp-3.11.17-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:fd89c4399ca67c0d2939211ecf8415747f4e2d855580f1efd9598eff433499ac"},
|
||||
{file = "aiohttp-3.11.17-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9b62134921162627482056379740998c54d51412a7e678a4d84a5510d6f634a7"},
|
||||
{file = "aiohttp-3.11.17-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:3e27e266396db6d6eee76769f66b356020b22e7ae8d7d4d47c9ced6bc53c62a0"},
|
||||
{file = "aiohttp-3.11.17-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0e93c46b7cc7476130bd396d63eafc6c458e74eddd1a1c65a2ac8e4bdb4cf1d1"},
|
||||
{file = "aiohttp-3.11.17-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7903749414023ad7d0a46cd4816d9ac99be7552635a0b3deae0b2c9a51d0cfa6"},
|
||||
{file = "aiohttp-3.11.17-cp310-cp310-win32.whl", hash = "sha256:de96c4aba1506b225cf1ab750fdd60e1f8d9d23add6431150a43fbef0542fe18"},
|
||||
{file = "aiohttp-3.11.17-cp310-cp310-win_amd64.whl", hash = "sha256:c08753a2c4d6f9175b234fd1ee227fc68a3c95ee09d52447af25b1ecb99d5886"},
|
||||
{file = "aiohttp-3.11.17-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9c10ede918e0d1edf9ef75cedb9ab303e509d0616020ecea42c427e3694a4d63"},
|
||||
{file = "aiohttp-3.11.17-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:127696d62e66badc3b554832343df031fd0012f080002d9074cfb735ce9c677e"},
|
||||
{file = "aiohttp-3.11.17-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26ac39d318507a7fb79b4eee31d7f92a7198d22c0d26d2c2dee4c945172ee509"},
|
||||
{file = "aiohttp-3.11.17-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb8e28f9b3b15ab1d840a7ffe0d7b63ee8bb01ef68c611f2ddbd5f96fa5f0e53"},
|
||||
{file = "aiohttp-3.11.17-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01e9d930cea36405ccdcde45921a29c7e22e0b581fc570f630d72a5bcac56c8c"},
|
||||
{file = "aiohttp-3.11.17-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dad404c74f4aad90b181db75d79338e87c852e638460003c78bdfd92c07fff3b"},
|
||||
{file = "aiohttp-3.11.17-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf30c77f5ddb1ccf568881f9076d0840f0b9a9c94dabe03126474768f951a48a"},
|
||||
{file = "aiohttp-3.11.17-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1987db5cb7e2c21693047f8f3c07df7bed3cb13403e5df5f684fe6a8478eff85"},
|
||||
{file = "aiohttp-3.11.17-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:612a5bc4e56a52986e89f1439fca86b765682613fdebc71c01de46736b33bd34"},
|
||||
{file = "aiohttp-3.11.17-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:398a021a7207a04960a8165f53f59e0c0b7cd54fe69ab7f0895f105e391a7964"},
|
||||
{file = "aiohttp-3.11.17-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e70a3672c734e6d792903e7b22d2f514ed8cbfa27c4a8e9171191da5c7d3c3b1"},
|
||||
{file = "aiohttp-3.11.17-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b51c9b38d904957ff2df58bf72874634d674b228c03f5d48e143996a8da5c819"},
|
||||
{file = "aiohttp-3.11.17-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a689fca65127d7599cfb0123c7dccb32d7eebe009d20bfa69ce93aff143dbbc1"},
|
||||
{file = "aiohttp-3.11.17-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:23120ea57904cfd920791afe5a780151cd8e99b039c35c38bb8a0d3e35af049b"},
|
||||
{file = "aiohttp-3.11.17-cp311-cp311-win32.whl", hash = "sha256:07e2c1c06c15cb95721670a69f3cbb1dae22b0914de6e362dba2228b6dd675e7"},
|
||||
{file = "aiohttp-3.11.17-cp311-cp311-win_amd64.whl", hash = "sha256:5e245caba8842f176ecc8ae1fde1ef0c89669614cb379c32e069cd0b96b3547f"},
|
||||
{file = "aiohttp-3.11.17-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:03ee8b587cc7bd345552235cd7117097c169f3a531a7239dc9a3c6b6db1cf46a"},
|
||||
{file = "aiohttp-3.11.17-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2bd255da118f96446567d9870621a07b8f36b1130826b07f2910ef1aeb4a85c0"},
|
||||
{file = "aiohttp-3.11.17-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2620d1cbeb688094045f06000b5b6127df2eb768cae07d95137b0e998cd6ce04"},
|
||||
{file = "aiohttp-3.11.17-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9eac3842f3258c77d35bfe93356fedbc6e5e943fd056534be71b694289b36973"},
|
||||
{file = "aiohttp-3.11.17-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:626dffeeeee34f2b5a327df05d6cb48ecfabcb141d56590d3c779accedc62d88"},
|
||||
{file = "aiohttp-3.11.17-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aded01d2bda55b2f62a361a2d55f8c9c04436eff6220e579ca7fb72cfe68b48a"},
|
||||
{file = "aiohttp-3.11.17-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b830715cb818efc4fbd7b9631661e3498b068f23680982d7586103333b0d4df"},
|
||||
{file = "aiohttp-3.11.17-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ac7ac3d0a7ab5ce1076c788640274f594397603381747b3d4142beec004c44a"},
|
||||
{file = "aiohttp-3.11.17-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9dba68088ecdd16b306513463f7e295699bc52bb09573d2bc3ff3d0e7bdc34ff"},
|
||||
{file = "aiohttp-3.11.17-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:80ba863e1e1055577f27a484b0f002b31297432016262d5f9d2dab5c6d21c5ad"},
|
||||
{file = "aiohttp-3.11.17-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:003c955924fa7d1b100599ce4f5da3ce68bd151b81b5a8c18369ba585766ed31"},
|
||||
{file = "aiohttp-3.11.17-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6e615b94364f7ea0dc95922c351e106ffb2eded09ffd7a7102ab2e202d17bdaa"},
|
||||
{file = "aiohttp-3.11.17-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:05eb25fa6e5495b3866a3b974fe3b214d5a0b6bb862cd54b7ec4d997948aa12c"},
|
||||
{file = "aiohttp-3.11.17-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3a3fb727360af3e94667e243cb21cf5069c0df9a70adcc81751136d53f526bc6"},
|
||||
{file = "aiohttp-3.11.17-cp312-cp312-win32.whl", hash = "sha256:93a7cfacf28887ddebd9c697d590002e8d52ac51acb4faf0d00abe1bb791339e"},
|
||||
{file = "aiohttp-3.11.17-cp312-cp312-win_amd64.whl", hash = "sha256:c43311c94200a63e31b62e82872c6dd43fe904cbb8f161c830bfe65c17583658"},
|
||||
{file = "aiohttp-3.11.17-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:57de66477af1176b3d25058594000eb32d21f82039909ef1bf865fe666c8c0ea"},
|
||||
{file = "aiohttp-3.11.17-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0d119d1e6d5bf930b285285aec70d66b7a7efa4914231441c7f606e86ca17e2"},
|
||||
{file = "aiohttp-3.11.17-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d0966f0c14be863cb51e0aa56366c45a2331f46a64347b49a0e9c687c72d43c1"},
|
||||
{file = "aiohttp-3.11.17-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f24d2b1f2516953b001736f82be7cf5d3234fc90e82a2d9a33ad8cdffb28f4c5"},
|
||||
{file = "aiohttp-3.11.17-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3197cd39307747127f7a3489b6aca4286c7613e8509e65f46702831136257d12"},
|
||||
{file = "aiohttp-3.11.17-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e4d134b96448c955e3e29e4026ee499441182bfd92f293accfa1cf1525a061b"},
|
||||
{file = "aiohttp-3.11.17-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20c68e4411440209fd64abfb27ff5e09e5a59a0fab4dbd07808e762e6fada670"},
|
||||
{file = "aiohttp-3.11.17-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1131f61afa4a4800fd5770ab13b1587bf4d07a0a561ee6f30f58c2300675ec3b"},
|
||||
{file = "aiohttp-3.11.17-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d3d4a383935ed1e39109e43535230b7e61781348fc2cf52c1006ae663dcbbc73"},
|
||||
{file = "aiohttp-3.11.17-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ada2a986cdec0caa51ee787b838441eeece50cd1ea075053fef51e0c995114b4"},
|
||||
{file = "aiohttp-3.11.17-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a118aae8008e209f100396bbd2d1d798aaa43651202c65cd2664680dd27aa061"},
|
||||
{file = "aiohttp-3.11.17-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2bf50db2e855226363aecb78353593987bf9a0b593434814b4a1f09586a116a4"},
|
||||
{file = "aiohttp-3.11.17-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:4f1c1db9470c9aa1ab45cee80b4b7db51fa4cfc01abc5e648d991266e676ac90"},
|
||||
{file = "aiohttp-3.11.17-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c4a92e2a228829c67f3c633a2329a8b85ed6cfe3b25047f48b4d51e92a655a87"},
|
||||
{file = "aiohttp-3.11.17-cp313-cp313-win32.whl", hash = "sha256:8962df1a29794f3204863ea6d93001a3d77cb1c4ee87f8c7683fe3fb6ec27373"},
|
||||
{file = "aiohttp-3.11.17-cp313-cp313-win_amd64.whl", hash = "sha256:71a1fd6421056980280fe490f211fe0f0c385271b42fb1440c4abcd891b2133a"},
|
||||
{file = "aiohttp-3.11.17-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:be033aceea6a8cd03cc2e2b6936beb485bb6b71907ad5a0e78bb942857b4f468"},
|
||||
{file = "aiohttp-3.11.17-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e7874d1e620159c18c99b87f37c45a8c30abbba2678d31ae3409d7c00c995f62"},
|
||||
{file = "aiohttp-3.11.17-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:919b411fae2376d5b78901949e556bd646e5de16e0631aed5a1b00ad55b5ec40"},
|
||||
{file = "aiohttp-3.11.17-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dffeb59c7f3414fd4da384cf76428c59b754e80d4b10d8e5d7018bed135d554d"},
|
||||
{file = "aiohttp-3.11.17-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ff5a5c9afbdaff09bf7b4a0655f1084d6c9f562fc33df8c0f7a785480405cf8"},
|
||||
{file = "aiohttp-3.11.17-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e4122d386a1695aee05f49a8bb4ff69c0e74b2b9db80a4d8157165327d07103"},
|
||||
{file = "aiohttp-3.11.17-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcb753ca25397eac1d38c7112c7b910feabef198d4e411aa5957857795681a25"},
|
||||
{file = "aiohttp-3.11.17-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29e0f47656b826d74034b612c11709842c20ebe373e398812208b84ec57f79a"},
|
||||
{file = "aiohttp-3.11.17-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:56a5acb7a715a1a1ad8ed424f7cbb2dc2950dc8b20ae2d83c2117ba5751223b1"},
|
||||
{file = "aiohttp-3.11.17-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:da0bee7635aa12036269ef8a13fd0fa5549ccfe5eb9ef6c3f0ad7cc574f12e5e"},
|
||||
{file = "aiohttp-3.11.17-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:adb504bdf0691101c7c4544e25545df467ea7239097ca6c67d01b27e500037b5"},
|
||||
{file = "aiohttp-3.11.17-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2e376d7f3fedb770106c40546d5f76a61e5ba3d4dac42cc60a8062022586cc"},
|
||||
{file = "aiohttp-3.11.17-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:79ae28f78f0a4a18a308d61647e3ee9cc1e641cb3ad5531db059eff68f3ee63c"},
|
||||
{file = "aiohttp-3.11.17-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ac9f6cdb02e7376060c4243f17a402b8d895747d788426ef3b8799e7a28a4ee0"},
|
||||
{file = "aiohttp-3.11.17-cp39-cp39-win32.whl", hash = "sha256:e3bddefb2cae68be01186c89d9a41024ae929aaf9e30b65d8977b719185f7e17"},
|
||||
{file = "aiohttp-3.11.17-cp39-cp39-win_amd64.whl", hash = "sha256:8e72767d1798770acdf27a20c523b1cd29973e487f6397b181ef0e7c583acb46"},
|
||||
{file = "aiohttp-3.11.17.tar.gz", hash = "sha256:2bf3ff374c3abd7a5c6c8de3ad7ed91e0e89a8b53353314c93766c3add5a208a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -1051,14 +1051,14 @@ pyyaml = ">=5.1"
|
|||
|
||||
[[package]]
|
||||
name = "mkdocs-material"
|
||||
version = "9.6.11"
|
||||
version = "9.6.12"
|
||||
description = "Documentation that simply works"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "mkdocs_material-9.6.11-py3-none-any.whl", hash = "sha256:47f21ef9cbf4f0ebdce78a2ceecaa5d413581a55141e4464902224ebbc0b1263"},
|
||||
{file = "mkdocs_material-9.6.11.tar.gz", hash = "sha256:0b7f4a0145c5074cdd692e4362d232fb25ef5b23328d0ec1ab287af77cc0deff"},
|
||||
{file = "mkdocs_material-9.6.12-py3-none-any.whl", hash = "sha256:92b4fbdc329e4febc267ca6e2c51e8501fa97b2225c5f4deb4d4e43550f8e61e"},
|
||||
{file = "mkdocs_material-9.6.12.tar.gz", hash = "sha256:add6a6337b29f9ea7912cb1efc661de2c369060b040eb5119855d794ea85b473"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -1529,14 +1529,14 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.11.2"
|
||||
version = "2.11.3"
|
||||
description = "Data validation using Python type hints"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "pydantic-2.11.2-py3-none-any.whl", hash = "sha256:7f17d25846bcdf89b670a86cdfe7b29a9f1c9ca23dee154221c9aa81845cfca7"},
|
||||
{file = "pydantic-2.11.2.tar.gz", hash = "sha256:2138628e050bd7a1e70b91d4bf4a91167f4ad76fdb83209b107c8d84b854917e"},
|
||||
{file = "pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f"},
|
||||
{file = "pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -2083,30 +2083,30 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.11.4"
|
||||
version = "0.11.6"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "ruff-0.11.4-py3-none-linux_armv6l.whl", hash = "sha256:d9f4a761ecbde448a2d3e12fb398647c7f0bf526dbc354a643ec505965824ed2"},
|
||||
{file = "ruff-0.11.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8c1747d903447d45ca3d40c794d1a56458c51e5cc1bc77b7b64bd2cf0b1626cc"},
|
||||
{file = "ruff-0.11.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:51a6494209cacca79e121e9b244dc30d3414dac8cc5afb93f852173a2ecfc906"},
|
||||
{file = "ruff-0.11.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f171605f65f4fc49c87f41b456e882cd0c89e4ac9d58e149a2b07930e1d466f"},
|
||||
{file = "ruff-0.11.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ebf99ea9af918878e6ce42098981fc8c1db3850fef2f1ada69fb1dcdb0f8e79e"},
|
||||
{file = "ruff-0.11.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edad2eac42279df12e176564a23fc6f4aaeeb09abba840627780b1bb11a9d223"},
|
||||
{file = "ruff-0.11.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f103a848be9ff379fc19b5d656c1f911d0a0b4e3e0424f9532ececf319a4296e"},
|
||||
{file = "ruff-0.11.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:193e6fac6eb60cc97b9f728e953c21cc38a20077ed64f912e9d62b97487f3f2d"},
|
||||
{file = "ruff-0.11.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7af4e5f69b7c138be8dcffa5b4a061bf6ba6a3301f632a6bce25d45daff9bc99"},
|
||||
{file = "ruff-0.11.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:126b1bf13154aa18ae2d6c3c5efe144ec14b97c60844cfa6eb960c2a05188222"},
|
||||
{file = "ruff-0.11.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8806daaf9dfa881a0ed603f8a0e364e4f11b6ed461b56cae2b1c0cab0645304"},
|
||||
{file = "ruff-0.11.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5d94bb1cc2fc94a769b0eb975344f1b1f3d294da1da9ddbb5a77665feb3a3019"},
|
||||
{file = "ruff-0.11.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:995071203d0fe2183fc7a268766fd7603afb9996785f086b0d76edee8755c896"},
|
||||
{file = "ruff-0.11.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7a37ca937e307ea18156e775a6ac6e02f34b99e8c23fe63c1996185a4efe0751"},
|
||||
{file = "ruff-0.11.4-py3-none-win32.whl", hash = "sha256:0e9365a7dff9b93af933dab8aebce53b72d8f815e131796268709890b4a83270"},
|
||||
{file = "ruff-0.11.4-py3-none-win_amd64.whl", hash = "sha256:5a9fa1c69c7815e39fcfb3646bbfd7f528fa8e2d4bebdcf4c2bd0fa037a255fb"},
|
||||
{file = "ruff-0.11.4-py3-none-win_arm64.whl", hash = "sha256:d435db6b9b93d02934cf61ef332e66af82da6d8c69aefdea5994c89997c7a0fc"},
|
||||
{file = "ruff-0.11.4.tar.gz", hash = "sha256:f45bd2fb1a56a5a85fae3b95add03fb185a0b30cf47f5edc92aa0355ca1d7407"},
|
||||
{file = "ruff-0.11.6-py3-none-linux_armv6l.whl", hash = "sha256:d84dcbe74cf9356d1bdb4a78cf74fd47c740bf7bdeb7529068f69b08272239a1"},
|
||||
{file = "ruff-0.11.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9bc583628e1096148011a5d51ff3c836f51899e61112e03e5f2b1573a9b726de"},
|
||||
{file = "ruff-0.11.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f2959049faeb5ba5e3b378709e9d1bf0cab06528b306b9dd6ebd2a312127964a"},
|
||||
{file = "ruff-0.11.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63c5d4e30d9d0de7fedbfb3e9e20d134b73a30c1e74b596f40f0629d5c28a193"},
|
||||
{file = "ruff-0.11.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a4b9a4e1439f7d0a091c6763a100cef8fbdc10d68593df6f3cfa5abdd9246e"},
|
||||
{file = "ruff-0.11.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b5edf270223dd622218256569636dc3e708c2cb989242262fe378609eccf1308"},
|
||||
{file = "ruff-0.11.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f55844e818206a9dd31ff27f91385afb538067e2dc0beb05f82c293ab84f7d55"},
|
||||
{file = "ruff-0.11.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d8f782286c5ff562e4e00344f954b9320026d8e3fae2ba9e6948443fafd9ffc"},
|
||||
{file = "ruff-0.11.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01c63ba219514271cee955cd0adc26a4083df1956d57847978383b0e50ffd7d2"},
|
||||
{file = "ruff-0.11.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15adac20ef2ca296dd3d8e2bedc6202ea6de81c091a74661c3666e5c4c223ff6"},
|
||||
{file = "ruff-0.11.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4dd6b09e98144ad7aec026f5588e493c65057d1b387dd937d7787baa531d9bc2"},
|
||||
{file = "ruff-0.11.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:45b2e1d6c0eed89c248d024ea95074d0e09988d8e7b1dad8d3ab9a67017a5b03"},
|
||||
{file = "ruff-0.11.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bd40de4115b2ec4850302f1a1d8067f42e70b4990b68838ccb9ccd9f110c5e8b"},
|
||||
{file = "ruff-0.11.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:77cda2dfbac1ab73aef5e514c4cbfc4ec1fbef4b84a44c736cc26f61b3814cd9"},
|
||||
{file = "ruff-0.11.6-py3-none-win32.whl", hash = "sha256:5151a871554be3036cd6e51d0ec6eef56334d74dfe1702de717a995ee3d5b287"},
|
||||
{file = "ruff-0.11.6-py3-none-win_amd64.whl", hash = "sha256:cce85721d09c51f3b782c331b0abd07e9d7d5f775840379c640606d3159cae0e"},
|
||||
{file = "ruff-0.11.6-py3-none-win_arm64.whl", hash = "sha256:3567ba0d07fb170b1b48d944715e3294b77f5b7679e8ba258199a250383ccb79"},
|
||||
{file = "ruff-0.11.6.tar.gz", hash = "sha256:bec8bcc3ac228a45ccc811e45f7eb61b950dbf4cf31a67fa89352574b01c7d79"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2406,4 +2406,4 @@ type = ["pytest-mypy"]
|
|||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = "^3.9"
|
||||
content-hash = "e9b71c7a85a236b7962a6d6d9fc5b0fd15c9a36a3fbeeab7736fe4a8c060d496"
|
||||
content-hash = "8d570a489cde6b6fe3eace984e3cba246f6f88bb0daba9739e1a61fd155b9091"
|
||||
|
|
|
@ -29,7 +29,7 @@ Authlib = "^1.2.0"
|
|||
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
ruff = ">=0.5.1,<0.11.5"
|
||||
ruff = ">=0.5.1,<0.11.7"
|
||||
pytest = "^8.3.4"
|
||||
mypy = "^1.14.1"
|
||||
types-requests = "^2.32.0.20241016"
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
use super::{QueryServerReadV1, QueryServerWriteV1};
|
||||
use kanidm_proto::scim_v1::{
|
||||
client::ScimFilter, server::ScimEntryKanidm, ScimEntryGetQuery, ScimSyncRequest, ScimSyncState,
|
||||
client::ScimEntryPostGeneric, client::ScimFilter, server::ScimEntryKanidm, ScimEntryGetQuery,
|
||||
ScimSyncRequest, ScimSyncState,
|
||||
};
|
||||
use kanidmd_lib::idm::scim::{
|
||||
GenerateScimSyncTokenEvent, ScimSyncFinaliseEvent, ScimSyncTerminateEvent, ScimSyncUpdateEvent,
|
||||
};
|
||||
|
||||
use kanidmd_lib::server::scim::{ScimCreateEvent, ScimDeleteEvent};
|
||||
|
||||
use kanidmd_lib::idm::server::IdmServerTransaction;
|
||||
use kanidmd_lib::prelude::*;
|
||||
|
||||
|
@ -176,6 +180,73 @@ impl QueryServerWriteV1 {
|
|||
.scim_sync_apply(&sse, &changes, ct)
|
||||
.and_then(|r| idms_prox_write.commit().map(|_| r))
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "info",
|
||||
skip_all,
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
pub async fn scim_entry_create(
|
||||
&self,
|
||||
client_auth_info: ClientAuthInfo,
|
||||
eventid: Uuid,
|
||||
classes: &[EntryClass],
|
||||
entry: ScimEntryPostGeneric,
|
||||
) -> Result<ScimEntryKanidm, OperationError> {
|
||||
let ct = duration_from_epoch_now();
|
||||
let mut idms_prox_write = self.idms.proxy_write(ct).await?;
|
||||
let ident = idms_prox_write
|
||||
.validate_client_auth_info_to_ident(client_auth_info, ct)
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Invalid identity");
|
||||
e
|
||||
})?;
|
||||
|
||||
let scim_create_event =
|
||||
ScimCreateEvent::try_from(ident, classes, entry, &mut idms_prox_write.qs_write)?;
|
||||
|
||||
idms_prox_write
|
||||
.qs_write
|
||||
.scim_create(scim_create_event)
|
||||
.and_then(|r| idms_prox_write.commit().map(|_| r))
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "info",
|
||||
skip_all,
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
pub async fn scim_entry_id_delete(
|
||||
&self,
|
||||
client_auth_info: ClientAuthInfo,
|
||||
eventid: Uuid,
|
||||
uuid_or_name: String,
|
||||
class: EntryClass,
|
||||
) -> Result<(), OperationError> {
|
||||
let ct = duration_from_epoch_now();
|
||||
let mut idms_prox_write = self.idms.proxy_write(ct).await?;
|
||||
let ident = idms_prox_write
|
||||
.validate_client_auth_info_to_ident(client_auth_info, ct)
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Invalid identity");
|
||||
e
|
||||
})?;
|
||||
|
||||
let target = idms_prox_write
|
||||
.qs_write
|
||||
.name_to_uuid(uuid_or_name.as_str())
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Error resolving id to target");
|
||||
e
|
||||
})?;
|
||||
|
||||
let scim_delete_event = ScimDeleteEvent::new(ident, target, class);
|
||||
|
||||
idms_prox_write
|
||||
.qs_write
|
||||
.scim_delete(scim_delete_event)
|
||||
.and_then(|r| idms_prox_write.commit().map(|_| r))
|
||||
}
|
||||
}
|
||||
|
||||
impl QueryServerReadV1 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -78,6 +78,8 @@ impl Modify for SecurityAddon {
|
|||
super::v1_scim::scim_sync_get,
|
||||
super::v1_scim::scim_entry_id_get,
|
||||
super::v1_scim::scim_person_id_get,
|
||||
super::v1_scim::scim_application_post,
|
||||
super::v1_scim::scim_application_id_delete,
|
||||
|
||||
super::v1::schema_get,
|
||||
super::v1::whoami,
|
||||
|
|
|
@ -43,7 +43,7 @@ fn figure_out_if_we_have_all_the_routes() {
|
|||
.unwrap();
|
||||
// work our way through the source files in this package looking for routedefs
|
||||
let mut found_routes: BTreeMap<String, Vec<(String, String)>> = BTreeMap::new();
|
||||
let walker = walkdir::WalkDir::new(format!("{}/src", env!("CARGO_MANIFEST_DIR")))
|
||||
let walker = walkdir::WalkDir::new(format!("{}/src/https", env!("CARGO_MANIFEST_DIR")))
|
||||
.follow_links(false)
|
||||
.into_iter();
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -9,10 +9,11 @@ use super::ServerState;
|
|||
use crate::https::extractors::VerifiedClientInformation;
|
||||
use axum::extract::{rejection::JsonRejection, DefaultBodyLimit, Path, Query, State};
|
||||
use axum::response::{Html, IntoResponse, Response};
|
||||
use axum::routing::{get, post};
|
||||
use axum::routing::{delete, get, post};
|
||||
use axum::{Extension, Json, Router};
|
||||
use kanidm_proto::scim_v1::{
|
||||
server::ScimEntryKanidm, ScimEntryGetQuery, ScimSyncRequest, ScimSyncState,
|
||||
client::ScimEntryPostGeneric, server::ScimEntryKanidm, ScimEntryGetQuery, ScimSyncRequest,
|
||||
ScimSyncState,
|
||||
};
|
||||
use kanidm_proto::v1::Entry as ProtoEntry;
|
||||
use kanidmd_lib::prelude::*;
|
||||
|
@ -383,6 +384,65 @@ async fn scim_person_id_get(
|
|||
.map_err(WebError::from)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/scim/v1/Application",
|
||||
responses(
|
||||
(status = 200, content_type="application/json", body=ScimEntry),
|
||||
ApiResponseWithout200,
|
||||
),
|
||||
security(("token_jwt" = [])),
|
||||
tag = "scim",
|
||||
operation_id = "scim_application_post"
|
||||
)]
|
||||
async fn scim_application_post(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
|
||||
Json(entry_post): Json<ScimEntryPostGeneric>,
|
||||
) -> Result<Json<ScimEntryKanidm>, WebError> {
|
||||
state
|
||||
.qe_w_ref
|
||||
.scim_entry_create(
|
||||
client_auth_info,
|
||||
kopid.eventid,
|
||||
&[
|
||||
EntryClass::Account,
|
||||
EntryClass::ServiceAccount,
|
||||
EntryClass::Application,
|
||||
],
|
||||
entry_post,
|
||||
)
|
||||
.await
|
||||
.map(Json::from)
|
||||
.map_err(WebError::from)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/scim/v1/Application/{id}",
|
||||
responses(
|
||||
(status = 200, content_type="application/json"),
|
||||
ApiResponseWithout200,
|
||||
),
|
||||
security(("token_jwt" = [])),
|
||||
tag = "scim",
|
||||
operation_id = "scim_application_id_delete"
|
||||
)]
|
||||
async fn scim_application_id_delete(
|
||||
State(state): State<ServerState>,
|
||||
Path(id): Path<String>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
|
||||
) -> Result<Json<()>, WebError> {
|
||||
state
|
||||
.qe_w_ref
|
||||
.scim_entry_id_delete(client_auth_info, kopid.eventid, id, EntryClass::Application)
|
||||
.await
|
||||
.map(Json::from)
|
||||
.map_err(WebError::from)
|
||||
}
|
||||
|
||||
pub fn route_setup() -> Router<ServerState> {
|
||||
Router::new()
|
||||
.route(
|
||||
|
@ -486,6 +546,17 @@ pub fn route_setup() -> Router<ServerState> {
|
|||
//
|
||||
// POST Send a sync update
|
||||
//
|
||||
//
|
||||
// Application /Application Post Create a new application
|
||||
//
|
||||
.route("/scim/v1/Application", post(scim_application_post))
|
||||
// Application /Application/{id} Delete Delete the application identified by id
|
||||
//
|
||||
.route(
|
||||
"/scim/v1/Application/:id",
|
||||
delete(scim_application_id_delete),
|
||||
)
|
||||
// Synchronisation routes.
|
||||
.route(
|
||||
"/scim/v1/Sync",
|
||||
post(scim_sync_post)
|
||||
|
|
|
@ -121,6 +121,7 @@ struct SetUnixCredPartial {
|
|||
struct AddSshPublicKeyPartial {
|
||||
title_error: Option<String>,
|
||||
key_error: Option<String>,
|
||||
key_value: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
|
@ -902,6 +903,7 @@ pub(crate) async fn view_add_ssh_publickey(
|
|||
return Ok((AddSshPublicKeyPartial {
|
||||
title_error: None,
|
||||
key_error: None,
|
||||
key_value: None,
|
||||
},)
|
||||
.into_response());
|
||||
}
|
||||
|
@ -920,6 +922,7 @@ pub(crate) async fn view_add_ssh_publickey(
|
|||
return Ok((AddSshPublicKeyPartial {
|
||||
title_error: None,
|
||||
key_error: Some("Key cannot be parsed".to_string()),
|
||||
key_value: Some(new_key.key),
|
||||
},)
|
||||
.into_response());
|
||||
}
|
||||
|
@ -965,6 +968,7 @@ pub(crate) async fn view_add_ssh_publickey(
|
|||
AddSshPublicKeyPartial {
|
||||
title_error,
|
||||
key_error,
|
||||
key_value: Some(new_key.key),
|
||||
},
|
||||
)
|
||||
.into_response())
|
||||
|
|
|
@ -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)),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<textarea class="form-control(% if let Some(_) = key_error %) is-invalid(% endif %)" id="key-content" rows="5" name="key"
|
||||
aria-describedby="key-validation-feedback"
|
||||
placeholder="Begins with 'ssh-rsa', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'ssh-ed25519', 'sk-ecdsa-sha2-nistp256@openssh.com', or 'sk-ssh-ed25519@openssh.com'"
|
||||
></textarea>
|
||||
>(% if let Some(key_value) = key_value %)(( key_value ))(% endif %)</textarea>
|
||||
<div id="key-validation-feedback" class="invalid-feedback">
|
||||
(% if let Some(key_error) = key_error %)(( key_error ))(% endif %)
|
||||
</div>
|
||||
|
|
|
@ -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());
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -334,6 +334,7 @@ pub const UUID_SCHEMA_ATTR_ACP_MODIFY_PRESENT_CLASS: Uuid =
|
|||
uuid!("00000000-0000-0000-0000-ffff00000189");
|
||||
pub const UUID_SCHEMA_ATTR_ACP_MODIFY_REMOVE_CLASS: Uuid =
|
||||
uuid!("00000000-0000-0000-0000-ffff00000190");
|
||||
pub const UUID_SCHEMA_ATTR_APPLICATION_URL: Uuid = uuid!("00000000-0000-0000-0000-ffff00000191");
|
||||
|
||||
// System and domain infos
|
||||
// I'd like to strongly criticise william of the past for making poor choices about these allocations.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -24,11 +24,6 @@
|
|||
//! [`filter`]: ../filter/index.html
|
||||
//! [`schema`]: ../schema/index.html
|
||||
|
||||
use std::cmp::Ordering;
|
||||
pub use std::collections::BTreeSet as Set;
|
||||
use std::collections::{BTreeMap as Map, BTreeMap, BTreeSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::be::dbentry::{DbEntry, DbEntryVers};
|
||||
use crate::be::dbvalue::DbValueSetV2;
|
||||
use crate::be::{IdxKey, IdxSlope};
|
||||
|
@ -41,7 +36,13 @@ use crate::prelude::*;
|
|||
use crate::repl::cid::Cid;
|
||||
use crate::repl::entry::EntryChangeState;
|
||||
use crate::repl::proto::{ReplEntryV1, ReplIncrementalEntryV1};
|
||||
use crate::schema::{SchemaAttribute, SchemaClass, SchemaTransaction};
|
||||
use crate::server::access::AccessEffectivePermission;
|
||||
use crate::value::{
|
||||
ApiToken, CredentialType, IndexType, IntentTokenState, Oauth2Session, PartialValue, Session,
|
||||
SyntaxType, Value,
|
||||
};
|
||||
use crate::valueset::{self, ScimResolveStatus, ValueSet};
|
||||
use compact_jwt::JwsEs256Signer;
|
||||
use hashbrown::{HashMap, HashSet};
|
||||
use kanidm_proto::internal::ImageValue;
|
||||
|
@ -53,6 +54,10 @@ use kanidm_proto::v1::Entry as ProtoEntry;
|
|||
use ldap3_proto::simple::{LdapPartialAttribute, LdapSearchResultEntry};
|
||||
use openssl::ec::EcKey;
|
||||
use openssl::pkey::{Private, Public};
|
||||
use std::cmp::Ordering;
|
||||
pub use std::collections::BTreeSet as Set;
|
||||
use std::collections::{BTreeMap as Map, BTreeMap, BTreeSet};
|
||||
use std::sync::Arc;
|
||||
use time::OffsetDateTime;
|
||||
use tracing::trace;
|
||||
use uuid::Uuid;
|
||||
|
@ -60,13 +65,6 @@ use webauthn_rs::prelude::{
|
|||
AttestationCaList, AttestedPasskey as AttestedPasskeyV4, Passkey as PasskeyV4,
|
||||
};
|
||||
|
||||
use crate::schema::{SchemaAttribute, SchemaClass, SchemaTransaction};
|
||||
use crate::value::{
|
||||
ApiToken, CredentialType, IndexType, IntentTokenState, Oauth2Session, PartialValue, Session,
|
||||
SyntaxType, Value,
|
||||
};
|
||||
use crate::valueset::{self, ScimResolveStatus, ValueSet};
|
||||
|
||||
pub type EntryInitNew = Entry<EntryInit, EntryNew>;
|
||||
pub type EntryInvalidNew = Entry<EntryInvalid, EntryNew>;
|
||||
pub type EntryRefreshNew = Entry<EntryRefresh, EntryNew>;
|
||||
|
@ -285,6 +283,18 @@ impl Default for Entry<EntryInit, EntryNew> {
|
|||
}
|
||||
}
|
||||
|
||||
impl FromIterator<(Attribute, ValueSet)> for EntryInitNew {
|
||||
fn from_iter<I: IntoIterator<Item = (Attribute, ValueSet)>>(iter: I) -> Self {
|
||||
let attrs = Eattrs::from_iter(iter);
|
||||
|
||||
Entry {
|
||||
valid: EntryInit,
|
||||
state: EntryNew,
|
||||
attrs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entry<EntryInit, EntryNew> {
|
||||
pub fn new() -> Self {
|
||||
Entry {
|
||||
|
@ -292,7 +302,6 @@ impl Entry<EntryInit, EntryNew> {
|
|||
valid: EntryInit,
|
||||
state: EntryNew,
|
||||
attrs: Map::new(),
|
||||
// attrs: Map::with_capacity(32),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -479,6 +488,11 @@ impl Entry<EntryInit, EntryNew> {
|
|||
self.attrs.remove(attr);
|
||||
}
|
||||
|
||||
/// Set the content of this ava with this valueset, ignoring the previous data.
|
||||
pub fn set_ava_set(&mut self, attr: &Attribute, vs: ValueSet) {
|
||||
self.attrs.insert(attr.clone(), vs);
|
||||
}
|
||||
|
||||
/// Replace the existing content of an attribute set of this Entry, with a new set of Values.
|
||||
pub fn set_ava<T>(&mut self, attr: Attribute, iter: T)
|
||||
where
|
||||
|
|
|
@ -346,6 +346,8 @@ pub struct CreateEvent {
|
|||
pub entries: Vec<Entry<EntryInit, EntryNew>>,
|
||||
// Is the CreateEvent from an internal or external source?
|
||||
// This may affect which plugins are run ...
|
||||
/// If true, the list of created entry UUID's will be returned.
|
||||
pub return_created_uuids: bool,
|
||||
}
|
||||
|
||||
impl CreateEvent {
|
||||
|
@ -363,7 +365,11 @@ impl CreateEvent {
|
|||
// What is the correct consuming iterator here? Can we
|
||||
// even do that?
|
||||
match rentries {
|
||||
Ok(entries) => Ok(CreateEvent { ident, entries }),
|
||||
Ok(entries) => Ok(CreateEvent {
|
||||
ident,
|
||||
entries,
|
||||
return_created_uuids: false,
|
||||
}),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
@ -373,13 +379,18 @@ impl CreateEvent {
|
|||
ident: Identity,
|
||||
entries: Vec<Entry<EntryInit, EntryNew>>,
|
||||
) -> Self {
|
||||
CreateEvent { ident, entries }
|
||||
CreateEvent {
|
||||
ident,
|
||||
entries,
|
||||
return_created_uuids: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_internal(entries: Vec<Entry<EntryInit, EntryNew>>) -> Self {
|
||||
CreateEvent {
|
||||
ident: Identity::from_internal(),
|
||||
entries,
|
||||
return_created_uuids: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -255,139 +255,6 @@ mod tests {
|
|||
|
||||
const TEST_CURRENT_TIME: u64 = 6000;
|
||||
|
||||
// Tests that only the correct combinations of [Account, Person, Application and
|
||||
// ServiceAccount] classes are allowed.
|
||||
#[idm_test]
|
||||
async fn test_idm_application_excludes(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
|
||||
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
||||
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
|
||||
|
||||
// ServiceAccount, Application and Person not allowed together
|
||||
let test_grp_name = "testgroup1";
|
||||
let test_grp_uuid = Uuid::new_v4();
|
||||
let e1 = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Group.to_value()),
|
||||
(Attribute::Name, Value::new_iname(test_grp_name)),
|
||||
(Attribute::Uuid, Value::Uuid(test_grp_uuid))
|
||||
);
|
||||
let test_entry_uuid = Uuid::new_v4();
|
||||
let e2 = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::ServiceAccount.to_value()),
|
||||
(Attribute::Class, EntryClass::Application.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname("test_app_name")),
|
||||
(Attribute::Uuid, Value::Uuid(test_entry_uuid)),
|
||||
(Attribute::Description, Value::new_utf8s("test_app_desc")),
|
||||
(
|
||||
Attribute::DisplayName,
|
||||
Value::new_utf8s("test_app_dispname")
|
||||
),
|
||||
(Attribute::LinkedGroup, Value::Refer(test_grp_uuid))
|
||||
);
|
||||
let ce = CreateEvent::new_internal(vec![e1, e2]);
|
||||
let cr = idms_prox_write.qs_write.create(&ce);
|
||||
assert!(cr.is_err());
|
||||
|
||||
// Application and Person not allowed together
|
||||
let test_grp_name = "testgroup1";
|
||||
let test_grp_uuid = Uuid::new_v4();
|
||||
let e1 = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Group.to_value()),
|
||||
(Attribute::Name, Value::new_iname(test_grp_name)),
|
||||
(Attribute::Uuid, Value::Uuid(test_grp_uuid))
|
||||
);
|
||||
let test_entry_uuid = Uuid::new_v4();
|
||||
let e2 = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Application.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname("test_app_name")),
|
||||
(Attribute::Uuid, Value::Uuid(test_entry_uuid)),
|
||||
(Attribute::Description, Value::new_utf8s("test_app_desc")),
|
||||
(
|
||||
Attribute::DisplayName,
|
||||
Value::new_utf8s("test_app_dispname")
|
||||
),
|
||||
(Attribute::LinkedGroup, Value::Refer(test_grp_uuid))
|
||||
);
|
||||
let ce = CreateEvent::new_internal(vec![e1, e2]);
|
||||
let cr = idms_prox_write.qs_write.create(&ce);
|
||||
assert!(cr.is_err());
|
||||
|
||||
// Supplements not satisfied, Application supplements ServiceAccount
|
||||
let test_grp_name = "testgroup1";
|
||||
let test_grp_uuid = Uuid::new_v4();
|
||||
let e1 = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Group.to_value()),
|
||||
(Attribute::Name, Value::new_iname(test_grp_name)),
|
||||
(Attribute::Uuid, Value::Uuid(test_grp_uuid))
|
||||
);
|
||||
let test_entry_uuid = Uuid::new_v4();
|
||||
let e2 = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Application.to_value()),
|
||||
(Attribute::Name, Value::new_iname("test_app_name")),
|
||||
(Attribute::Uuid, Value::Uuid(test_entry_uuid)),
|
||||
(Attribute::Description, Value::new_utf8s("test_app_desc")),
|
||||
(Attribute::LinkedGroup, Value::Refer(test_grp_uuid))
|
||||
);
|
||||
let ce = CreateEvent::new_internal(vec![e1, e2]);
|
||||
let cr = idms_prox_write.qs_write.create(&ce);
|
||||
assert!(cr.is_err());
|
||||
|
||||
// Supplements not satisfied, Application supplements ServiceAccount
|
||||
let test_grp_name = "testgroup1";
|
||||
let test_grp_uuid = Uuid::new_v4();
|
||||
let e1 = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Group.to_value()),
|
||||
(Attribute::Name, Value::new_iname(test_grp_name)),
|
||||
(Attribute::Uuid, Value::Uuid(test_grp_uuid))
|
||||
);
|
||||
let test_entry_uuid = Uuid::new_v4();
|
||||
let e2 = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Application.to_value()),
|
||||
(Attribute::Name, Value::new_iname("test_app_name")),
|
||||
(Attribute::Uuid, Value::Uuid(test_entry_uuid)),
|
||||
(Attribute::Description, Value::new_utf8s("test_app_desc")),
|
||||
(Attribute::LinkedGroup, Value::Refer(test_grp_uuid))
|
||||
);
|
||||
let ce = CreateEvent::new_internal(vec![e1, e2]);
|
||||
let cr = idms_prox_write.qs_write.create(&ce);
|
||||
assert!(cr.is_err());
|
||||
|
||||
// Supplements satisfied, Application supplements ServiceAccount
|
||||
let test_grp_name = "testgroup1";
|
||||
let test_grp_uuid = Uuid::new_v4();
|
||||
let e1 = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Group.to_value()),
|
||||
(Attribute::Name, Value::new_iname(test_grp_name)),
|
||||
(Attribute::Uuid, Value::Uuid(test_grp_uuid))
|
||||
);
|
||||
let test_entry_uuid = Uuid::new_v4();
|
||||
let e2 = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Application.to_value()),
|
||||
(Attribute::Class, EntryClass::ServiceAccount.to_value()),
|
||||
(Attribute::Name, Value::new_iname("test_app_name")),
|
||||
(Attribute::Uuid, Value::Uuid(test_entry_uuid)),
|
||||
(Attribute::Description, Value::new_utf8s("test_app_desc")),
|
||||
(Attribute::LinkedGroup, Value::Refer(test_grp_uuid))
|
||||
);
|
||||
let ce = CreateEvent::new_internal(vec![e1, e2]);
|
||||
let cr = idms_prox_write.qs_write.create(&ce);
|
||||
assert!(cr.is_ok());
|
||||
}
|
||||
|
||||
// Tests it is not possible to create an application without the linked group attribute
|
||||
#[idm_test]
|
||||
async fn test_idm_application_no_linked_group(
|
||||
|
@ -404,6 +271,7 @@ mod tests {
|
|||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::ServiceAccount.to_value()),
|
||||
(Attribute::Class, EntryClass::Application.to_value()),
|
||||
(Attribute::DisplayName, Value::new_utf8s("Application")),
|
||||
(Attribute::Name, Value::new_iname("test_app_name")),
|
||||
(Attribute::Uuid, Value::Uuid(test_entry_uuid)),
|
||||
(Attribute::Description, Value::new_utf8s("test_app_desc")),
|
||||
|
@ -547,8 +415,10 @@ mod tests {
|
|||
|
||||
let e3 = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::ServiceAccount.to_value()),
|
||||
(Attribute::Class, EntryClass::Application.to_value()),
|
||||
(Attribute::DisplayName, Value::new_utf8s("Application")),
|
||||
(Attribute::Name, Value::new_iname(test_app_name)),
|
||||
(Attribute::Uuid, Value::Uuid(test_app_uuid)),
|
||||
(Attribute::LinkedGroup, Value::Refer(test_grp_uuid))
|
||||
|
@ -647,7 +517,9 @@ mod tests {
|
|||
let e2 = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::ServiceAccount.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Application.to_value()),
|
||||
(Attribute::DisplayName, Value::new_utf8s("Application")),
|
||||
(Attribute::Name, Value::new_iname("test_app_name")),
|
||||
(Attribute::Uuid, Value::Uuid(test_entry_uuid)),
|
||||
(Attribute::Description, Value::new_utf8s("test_app_desc")),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1119,8 +1119,10 @@ mod tests {
|
|||
|
||||
let e3 = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::ServiceAccount.to_value()),
|
||||
(Attribute::Class, EntryClass::Application.to_value()),
|
||||
(Attribute::DisplayName, Value::new_utf8s("Application")),
|
||||
(Attribute::Name, Value::new_iname(app_name)),
|
||||
(Attribute::Uuid, Value::Uuid(app_uuid)),
|
||||
(Attribute::LinkedGroup, Value::Refer(grp_uuid))
|
||||
|
@ -1209,7 +1211,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 +1234,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()),
|
||||
|
@ -1283,8 +1285,10 @@ mod tests {
|
|||
|
||||
let e3 = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::ServiceAccount.to_value()),
|
||||
(Attribute::Class, EntryClass::Application.to_value()),
|
||||
(Attribute::DisplayName, Value::new_utf8s("Application")),
|
||||
(Attribute::Name, Value::new_iname("testapp1")),
|
||||
(Attribute::Uuid, Value::Uuid(app_uuid)),
|
||||
(Attribute::LinkedGroup, Value::Refer(grp_uuid))
|
||||
|
@ -1456,8 +1460,10 @@ mod tests {
|
|||
|
||||
let e4 = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::ServiceAccount.to_value()),
|
||||
(Attribute::Class, EntryClass::Application.to_value()),
|
||||
(Attribute::DisplayName, Value::new_utf8s("Application")),
|
||||
(Attribute::Name, Value::new_iname(app1_name)),
|
||||
(Attribute::Uuid, Value::Uuid(app1_uuid)),
|
||||
(Attribute::LinkedGroup, Value::Refer(grp1_uuid))
|
||||
|
@ -1465,8 +1471,10 @@ mod tests {
|
|||
|
||||
let e5 = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::ServiceAccount.to_value()),
|
||||
(Attribute::Class, EntryClass::Application.to_value()),
|
||||
(Attribute::DisplayName, Value::new_utf8s("Application")),
|
||||
(Attribute::Name, Value::new_iname(app2_name)),
|
||||
(Attribute::Uuid, Value::Uuid(app2_uuid)),
|
||||
(Attribute::LinkedGroup, Value::Refer(grp2_uuid))
|
||||
|
@ -1651,8 +1659,10 @@ mod tests {
|
|||
|
||||
let e3 = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::ServiceAccount.to_value()),
|
||||
(Attribute::Class, EntryClass::Application.to_value()),
|
||||
(Attribute::DisplayName, Value::new_utf8s("Application")),
|
||||
(Attribute::Name, Value::new_iname(app1_name)),
|
||||
(Attribute::Uuid, Value::Uuid(app1_uuid)),
|
||||
(Attribute::LinkedGroup, Value::Refer(grp1_uuid))
|
||||
|
@ -2693,8 +2703,10 @@ mod tests {
|
|||
|
||||
let e3 = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::ServiceAccount.to_value()),
|
||||
(Attribute::Class, EntryClass::Application.to_value()),
|
||||
(Attribute::DisplayName, Value::new_utf8s("Application")),
|
||||
(Attribute::Name, Value::new_iname(app_name)),
|
||||
(Attribute::Uuid, Value::Uuid(app_uuid)),
|
||||
(Attribute::LinkedGroup, Value::Refer(grp_uuid))
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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| {
|
||||
|
|
|
@ -105,6 +105,7 @@ pub fn phase_1_schema_attrs() -> Vec<EntryInitNew> {
|
|||
// DL10
|
||||
SCHEMA_ATTR_DENIED_NAME_DL10.clone().into(),
|
||||
SCHEMA_ATTR_LDAP_MAXIMUM_QUERYABLE_ATTRIBUTES.clone().into(),
|
||||
SCHEMA_ATTR_APPLICATION_URL.clone().into(),
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -134,7 +135,7 @@ pub fn phase_2_schema_classes() -> Vec<EntryInitNew> {
|
|||
SCHEMA_CLASS_CLIENT_CERTIFICATE_DL7.clone().into(),
|
||||
// DL8
|
||||
SCHEMA_CLASS_ACCOUNT_POLICY_DL8.clone().into(),
|
||||
SCHEMA_CLASS_APPLICATION_DL8.clone().into(),
|
||||
SCHEMA_CLASS_APPLICATION.clone().into(),
|
||||
SCHEMA_CLASS_PERSON_DL8.clone().into(),
|
||||
// DL9
|
||||
SCHEMA_CLASS_OAUTH2_RS_DL9.clone().into(),
|
||||
|
|
|
@ -729,6 +729,14 @@ pub static ref SCHEMA_ATTR_APPLICATION_PASSWORD_DL8: SchemaAttribute = SchemaAtt
|
|||
..Default::default()
|
||||
};
|
||||
|
||||
pub static ref SCHEMA_ATTR_APPLICATION_URL: SchemaAttribute = SchemaAttribute {
|
||||
uuid: UUID_SCHEMA_ATTR_APPLICATION_URL,
|
||||
name: Attribute::ApplicationUrl,
|
||||
description: "The URL of an external application".to_string(),
|
||||
syntax: SyntaxType::Url,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// === classes ===
|
||||
pub static ref SCHEMA_CLASS_PERSON_DL8: SchemaClass = SchemaClass {
|
||||
uuid: UUID_SCHEMA_CLASS_PERSON,
|
||||
|
@ -838,9 +846,9 @@ pub static ref SCHEMA_CLASS_ACCOUNT_DL5: SchemaClass = SchemaClass {
|
|||
Attribute::Spn
|
||||
],
|
||||
systemsupplements: vec![
|
||||
EntryClass::OAuth2ResourceServer.into(),
|
||||
EntryClass::Person.into(),
|
||||
EntryClass::ServiceAccount.into(),
|
||||
EntryClass::OAuth2ResourceServer.into(),
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
@ -1082,13 +1090,20 @@ pub static ref SCHEMA_CLASS_CLIENT_CERTIFICATE_DL7: SchemaClass = SchemaClass {
|
|||
..Default::default()
|
||||
};
|
||||
|
||||
pub static ref SCHEMA_CLASS_APPLICATION_DL8: SchemaClass = SchemaClass {
|
||||
pub static ref SCHEMA_CLASS_APPLICATION: SchemaClass = SchemaClass {
|
||||
uuid: UUID_SCHEMA_CLASS_APPLICATION,
|
||||
name: EntryClass::Application.into(),
|
||||
|
||||
description: "The class representing an application".to_string(),
|
||||
systemmust: vec![Attribute::Name, Attribute::LinkedGroup],
|
||||
systemmay: vec![Attribute::Description],
|
||||
systemmust: vec![Attribute::LinkedGroup],
|
||||
systemmay: vec![
|
||||
Attribute::ApplicationUrl,
|
||||
],
|
||||
// I think this could change before release - I can see a world
|
||||
// whe we may want an oauth2 application to have application passwords,
|
||||
// or for this to be it's own thing. But service accounts also don't
|
||||
// quite do enough, they have api tokens, but that's all we kind
|
||||
// of want from them?
|
||||
systemsupplements: vec![EntryClass::ServiceAccount.into()],
|
||||
..Default::default()
|
||||
};
|
||||
|
|
|
@ -365,7 +365,7 @@ mod tests {
|
|||
let create = vec![e];
|
||||
|
||||
run_create_test!(
|
||||
Ok(()),
|
||||
Ok(None),
|
||||
preload,
|
||||
create,
|
||||
None,
|
||||
|
@ -468,7 +468,7 @@ mod tests {
|
|||
let create = vec![e];
|
||||
|
||||
run_create_test!(
|
||||
Ok(()),
|
||||
Ok(None),
|
||||
preload,
|
||||
create,
|
||||
None,
|
||||
|
|
|
@ -205,7 +205,7 @@ mod tests {
|
|||
|
||||
let create = vec![e];
|
||||
|
||||
run_create_test!(Ok(()), preload, create, None, |_| {});
|
||||
run_create_test!(Ok(None), preload, create, None, |_| {});
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -464,7 +464,7 @@ mod tests {
|
|||
let create = vec![e_dyn];
|
||||
|
||||
run_create_test!(
|
||||
Ok(()),
|
||||
Ok(None),
|
||||
preload,
|
||||
create,
|
||||
None,
|
||||
|
@ -513,7 +513,7 @@ mod tests {
|
|||
let create = vec![e_group];
|
||||
|
||||
run_create_test!(
|
||||
Ok(()),
|
||||
Ok(None),
|
||||
preload,
|
||||
create,
|
||||
None,
|
||||
|
@ -562,7 +562,7 @@ mod tests {
|
|||
let create = vec![e_group];
|
||||
|
||||
run_create_test!(
|
||||
Ok(()),
|
||||
Ok(None),
|
||||
preload,
|
||||
create,
|
||||
None,
|
||||
|
@ -607,7 +607,7 @@ mod tests {
|
|||
let create = vec![e_dyn, e_group];
|
||||
|
||||
run_create_test!(
|
||||
Ok(()),
|
||||
Ok(None),
|
||||
preload,
|
||||
create,
|
||||
None,
|
||||
|
|
|
@ -108,7 +108,7 @@ mod tests {
|
|||
|
||||
let create = vec![ea];
|
||||
run_create_test!(
|
||||
Ok(()),
|
||||
Ok(None),
|
||||
preload,
|
||||
create,
|
||||
None,
|
||||
|
|
|
@ -136,7 +136,7 @@ mod tests {
|
|||
let create = vec![e];
|
||||
|
||||
run_create_test!(
|
||||
Ok(()),
|
||||
Ok(None),
|
||||
preload,
|
||||
create,
|
||||
None,
|
||||
|
|
|
@ -858,7 +858,7 @@ mod tests {
|
|||
let preload = Vec::with_capacity(0);
|
||||
let create = vec![ea, eb];
|
||||
run_create_test!(
|
||||
Ok(()),
|
||||
Ok(None),
|
||||
preload,
|
||||
create,
|
||||
None,
|
||||
|
@ -889,7 +889,7 @@ mod tests {
|
|||
let preload = Vec::with_capacity(0);
|
||||
let create = vec![ea, eb, ec];
|
||||
run_create_test!(
|
||||
Ok(()),
|
||||
Ok(None),
|
||||
preload,
|
||||
create,
|
||||
None,
|
||||
|
@ -941,7 +941,7 @@ mod tests {
|
|||
let preload = Vec::with_capacity(0);
|
||||
let create = vec![ea, eb, ec];
|
||||
run_create_test!(
|
||||
Ok(()),
|
||||
Ok(None),
|
||||
preload,
|
||||
create,
|
||||
None,
|
||||
|
@ -999,7 +999,7 @@ mod tests {
|
|||
let preload = Vec::with_capacity(0);
|
||||
let create = vec![ea, eb, ec, ed];
|
||||
run_create_test!(
|
||||
Ok(()),
|
||||
Ok(None),
|
||||
preload,
|
||||
create,
|
||||
None,
|
||||
|
|
|
@ -181,7 +181,7 @@ mod tests {
|
|||
let preload = Vec::with_capacity(0);
|
||||
let create = vec![ea];
|
||||
run_create_test!(
|
||||
Ok(()),
|
||||
Ok(None),
|
||||
preload,
|
||||
create,
|
||||
None,
|
||||
|
|
|
@ -501,7 +501,7 @@ mod tests {
|
|||
let create = vec![eb];
|
||||
|
||||
run_create_test!(
|
||||
Ok(()),
|
||||
Ok(None),
|
||||
preload,
|
||||
create,
|
||||
None,
|
||||
|
@ -534,7 +534,7 @@ mod tests {
|
|||
let create = vec![e_group];
|
||||
|
||||
run_create_test!(
|
||||
Ok(()),
|
||||
Ok(None),
|
||||
preload,
|
||||
create,
|
||||
None,
|
||||
|
|
|
@ -233,7 +233,7 @@ mod tests {
|
|||
let preload = Vec::with_capacity(0);
|
||||
|
||||
run_create_test!(
|
||||
Ok(()),
|
||||
Ok(None),
|
||||
preload,
|
||||
create,
|
||||
None,
|
||||
|
@ -286,7 +286,7 @@ mod tests {
|
|||
let preload = Vec::with_capacity(0);
|
||||
|
||||
run_create_test!(
|
||||
Ok(()),
|
||||
Ok(None),
|
||||
preload,
|
||||
create,
|
||||
None,
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -7,7 +7,7 @@ impl QueryServerWriteTransaction<'_> {
|
|||
/// The create event is a raw, read only representation of the request
|
||||
/// that was made to us, including information about the identity
|
||||
/// performing the request.
|
||||
pub fn create(&mut self, ce: &CreateEvent) -> Result<(), OperationError> {
|
||||
pub fn create(&mut self, ce: &CreateEvent) -> Result<Option<Vec<Uuid>>, OperationError> {
|
||||
if !ce.ident.is_internal() {
|
||||
security_info!(name = %ce.ident, "create initiator");
|
||||
}
|
||||
|
@ -174,7 +174,12 @@ impl QueryServerWriteTransaction<'_> {
|
|||
} else {
|
||||
admin_info!("Create operation success");
|
||||
}
|
||||
Ok(())
|
||||
|
||||
if ce.return_created_uuids {
|
||||
Ok(Some(commit_cand.iter().map(|e| e.get_uuid()).collect()))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn internal_create(
|
||||
|
@ -182,7 +187,7 @@ impl QueryServerWriteTransaction<'_> {
|
|||
entries: Vec<Entry<EntryInit, EntryNew>>,
|
||||
) -> Result<(), OperationError> {
|
||||
let ce = CreateEvent::new_internal(entries);
|
||||
self.create(&ce)
|
||||
self.create(&ce).map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -28,8 +28,6 @@ use crate::schema::{
|
|||
SchemaWriteTransaction,
|
||||
};
|
||||
use crate::value::{CredentialType, EXTRACT_VAL_DN};
|
||||
use crate::valueset::uuid_to_proto_string;
|
||||
use crate::valueset::ScimValueIntermediate;
|
||||
use crate::valueset::*;
|
||||
use concread::arcache::{ARCacheBuilder, ARCacheReadTxn, ARCacheWriteTxn};
|
||||
use concread::cowcell::*;
|
||||
|
@ -1004,138 +1002,6 @@ pub trait QueryServerTransaction<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
fn resolve_scim_json_put(
|
||||
&mut self,
|
||||
attr: &Attribute,
|
||||
value: Option<JsonValue>,
|
||||
) -> Result<Option<ValueSet>, OperationError> {
|
||||
let schema = self.get_schema();
|
||||
// Lookup the attr
|
||||
let Some(schema_a) = schema.get_attributes().get(attr) else {
|
||||
// No attribute of this name exists - fail fast, there is no point to
|
||||
// proceed, as nothing can be satisfied.
|
||||
return Err(OperationError::InvalidAttributeName(attr.to_string()));
|
||||
};
|
||||
|
||||
let Some(value) = value else {
|
||||
// It's a none so the value needs to be unset, and the attr DOES exist in
|
||||
// schema.
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let resolve_status = match schema_a.syntax {
|
||||
SyntaxType::Utf8String => ValueSetUtf8::from_scim_json_put(value),
|
||||
SyntaxType::Utf8StringInsensitive => ValueSetIutf8::from_scim_json_put(value),
|
||||
SyntaxType::Uuid => ValueSetUuid::from_scim_json_put(value),
|
||||
SyntaxType::Boolean => ValueSetBool::from_scim_json_put(value),
|
||||
SyntaxType::SyntaxId => ValueSetSyntax::from_scim_json_put(value),
|
||||
SyntaxType::IndexId => ValueSetIndex::from_scim_json_put(value),
|
||||
SyntaxType::ReferenceUuid => ValueSetRefer::from_scim_json_put(value),
|
||||
SyntaxType::Utf8StringIname => ValueSetIname::from_scim_json_put(value),
|
||||
SyntaxType::NsUniqueId => ValueSetNsUniqueId::from_scim_json_put(value),
|
||||
SyntaxType::DateTime => ValueSetDateTime::from_scim_json_put(value),
|
||||
SyntaxType::EmailAddress => ValueSetEmailAddress::from_scim_json_put(value),
|
||||
SyntaxType::Url => ValueSetUrl::from_scim_json_put(value),
|
||||
SyntaxType::OauthScope => ValueSetOauthScope::from_scim_json_put(value),
|
||||
SyntaxType::OauthScopeMap => ValueSetOauthScopeMap::from_scim_json_put(value),
|
||||
SyntaxType::OauthClaimMap => ValueSetOauthClaimMap::from_scim_json_put(value),
|
||||
SyntaxType::UiHint => ValueSetUiHint::from_scim_json_put(value),
|
||||
SyntaxType::CredentialType => ValueSetCredentialType::from_scim_json_put(value),
|
||||
SyntaxType::Certificate => ValueSetCertificate::from_scim_json_put(value),
|
||||
SyntaxType::SshKey => ValueSetSshKey::from_scim_json_put(value),
|
||||
SyntaxType::Uint32 => ValueSetUint32::from_scim_json_put(value),
|
||||
|
||||
// Not Yet ... if ever
|
||||
// SyntaxType::JsonFilter => ValueSetJsonFilter::from_scim_json_put(value),
|
||||
SyntaxType::JsonFilter => Err(OperationError::InvalidAttribute(
|
||||
"Json Filters are not able to be set.".to_string(),
|
||||
)),
|
||||
// Can't be set currently as these are only internally generated for key-id's
|
||||
// SyntaxType::HexString => ValueSetHexString::from_scim_json_put(value),
|
||||
SyntaxType::HexString => Err(OperationError::InvalidAttribute(
|
||||
"Hex strings are not able to be set.".to_string(),
|
||||
)),
|
||||
|
||||
// Can't be set until we have better error handling in the set paths
|
||||
// SyntaxType::Image => ValueSetImage::from_scim_json_put(value),
|
||||
SyntaxType::Image => Err(OperationError::InvalidAttribute(
|
||||
"Images are not able to be set.".to_string(),
|
||||
)),
|
||||
|
||||
// Can't be set yet, mostly as I'm lazy
|
||||
// SyntaxType::WebauthnAttestationCaList => {
|
||||
// ValueSetWebauthnAttestationCaList::from_scim_json_put(value)
|
||||
// }
|
||||
SyntaxType::WebauthnAttestationCaList => Err(OperationError::InvalidAttribute(
|
||||
"Webauthn Attestation Ca Lists are not able to be set.".to_string(),
|
||||
)),
|
||||
|
||||
// Syntax types that can not be submitted
|
||||
SyntaxType::Credential => Err(OperationError::InvalidAttribute(
|
||||
"Credentials are not able to be set.".to_string(),
|
||||
)),
|
||||
SyntaxType::SecretUtf8String => Err(OperationError::InvalidAttribute(
|
||||
"Secrets are not able to be set.".to_string(),
|
||||
)),
|
||||
SyntaxType::SecurityPrincipalName => Err(OperationError::InvalidAttribute(
|
||||
"SPNs are not able to be set.".to_string(),
|
||||
)),
|
||||
SyntaxType::Cid => Err(OperationError::InvalidAttribute(
|
||||
"CIDs are not able to be set.".to_string(),
|
||||
)),
|
||||
SyntaxType::PrivateBinary => Err(OperationError::InvalidAttribute(
|
||||
"Private Binaries are not able to be set.".to_string(),
|
||||
)),
|
||||
SyntaxType::IntentToken => Err(OperationError::InvalidAttribute(
|
||||
"Intent Tokens are not able to be set.".to_string(),
|
||||
)),
|
||||
SyntaxType::Passkey => Err(OperationError::InvalidAttribute(
|
||||
"Passkeys are not able to be set.".to_string(),
|
||||
)),
|
||||
SyntaxType::AttestedPasskey => Err(OperationError::InvalidAttribute(
|
||||
"Attested Passkeys are not able to be set.".to_string(),
|
||||
)),
|
||||
SyntaxType::Session => Err(OperationError::InvalidAttribute(
|
||||
"Sessions are not able to be set.".to_string(),
|
||||
)),
|
||||
SyntaxType::JwsKeyEs256 => Err(OperationError::InvalidAttribute(
|
||||
"Jws ES256 Private Keys are not able to be set.".to_string(),
|
||||
)),
|
||||
SyntaxType::JwsKeyRs256 => Err(OperationError::InvalidAttribute(
|
||||
"Jws RS256 Private Keys are not able to be set.".to_string(),
|
||||
)),
|
||||
SyntaxType::Oauth2Session => Err(OperationError::InvalidAttribute(
|
||||
"Sessions are not able to be set.".to_string(),
|
||||
)),
|
||||
SyntaxType::TotpSecret => Err(OperationError::InvalidAttribute(
|
||||
"TOTP Secrets are not able to be set.".to_string(),
|
||||
)),
|
||||
SyntaxType::ApiToken => Err(OperationError::InvalidAttribute(
|
||||
"API Tokens are not able to be set.".to_string(),
|
||||
)),
|
||||
SyntaxType::AuditLogString => Err(OperationError::InvalidAttribute(
|
||||
"Audit Strings are not able to be set.".to_string(),
|
||||
)),
|
||||
SyntaxType::EcKeyPrivate => Err(OperationError::InvalidAttribute(
|
||||
"EC Private Keys are not able to be set.".to_string(),
|
||||
)),
|
||||
SyntaxType::KeyInternal => Err(OperationError::InvalidAttribute(
|
||||
"Key Internal Structures are not able to be set.".to_string(),
|
||||
)),
|
||||
SyntaxType::ApplicationPassword => Err(OperationError::InvalidAttribute(
|
||||
"Application Passwords are not able to be set.".to_string(),
|
||||
)),
|
||||
}?;
|
||||
|
||||
match resolve_status {
|
||||
ValueSetResolveStatus::Resolved(vs) => Ok(vs),
|
||||
ValueSetResolveStatus::NeedsResolution(vs_inter) => {
|
||||
self.resolve_valueset_intermediate(vs_inter)
|
||||
}
|
||||
}
|
||||
.map(Some)
|
||||
}
|
||||
|
||||
fn resolve_valueset_intermediate(
|
||||
&mut self,
|
||||
vs_inter: ValueSetIntermediate,
|
||||
|
|
|
@ -1,23 +1,27 @@
|
|||
use crate::prelude::*;
|
||||
use crate::schema::{SchemaAttribute, SchemaTransaction};
|
||||
use crate::server::batch_modify::{BatchModifyEvent, ModSetValid};
|
||||
use kanidm_proto::scim_v1::client::ScimEntryPutGeneric;
|
||||
use crate::server::ValueSetResolveStatus;
|
||||
use crate::valueset::*;
|
||||
use kanidm_proto::scim_v1::client::{ScimEntryPostGeneric, ScimEntryPutGeneric};
|
||||
use kanidm_proto::scim_v1::JsonValue;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug)]
|
||||
pub struct ScimEntryPutEvent {
|
||||
/// The identity performing the change.
|
||||
pub ident: Identity,
|
||||
pub(crate) ident: Identity,
|
||||
|
||||
// future - etags to detect version changes.
|
||||
/// The target entry that will be changed
|
||||
pub target: Uuid,
|
||||
pub(crate) target: Uuid,
|
||||
/// Update an attribute to contain the following value state.
|
||||
/// If the attribute is None, it is removed.
|
||||
pub attrs: BTreeMap<Attribute, Option<ValueSet>>,
|
||||
pub(crate) attrs: BTreeMap<Attribute, Option<ValueSet>>,
|
||||
|
||||
/// If an effective access check should be carried out post modification
|
||||
/// of the entries
|
||||
pub effective_access_check: bool,
|
||||
pub(crate) effective_access_check: bool,
|
||||
}
|
||||
|
||||
impl ScimEntryPutEvent {
|
||||
|
@ -48,6 +52,60 @@ impl ScimEntryPutEvent {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ScimCreateEvent {
|
||||
pub(crate) ident: Identity,
|
||||
pub(crate) entry: EntryInitNew,
|
||||
}
|
||||
|
||||
impl ScimCreateEvent {
|
||||
pub fn try_from(
|
||||
ident: Identity,
|
||||
classes: &[EntryClass],
|
||||
entry: ScimEntryPostGeneric,
|
||||
qs: &mut QueryServerWriteTransaction,
|
||||
) -> Result<Self, OperationError> {
|
||||
let mut entry = entry
|
||||
.attrs
|
||||
.into_iter()
|
||||
.map(|(attr, json_value)| {
|
||||
qs.resolve_scim_json_post(&attr, json_value)
|
||||
.map(|kani_value| (attr, kani_value))
|
||||
})
|
||||
.collect::<Result<EntryInitNew, _>>()?;
|
||||
|
||||
let classes = ValueSetIutf8::from_iter(classes.iter().map(|cls| cls.as_ref()))
|
||||
.ok_or(OperationError::SC0027ClassSetInvalid)?;
|
||||
|
||||
entry.set_ava_set(&Attribute::Class, classes);
|
||||
|
||||
Ok(ScimCreateEvent { ident, entry })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ScimDeleteEvent {
|
||||
/// The identity performing the change.
|
||||
pub(crate) ident: Identity,
|
||||
|
||||
// future - etags to detect version changes.
|
||||
/// The target entry that will be changed
|
||||
pub(crate) target: Uuid,
|
||||
|
||||
/// The class of the target entry.
|
||||
pub(crate) class: EntryClass,
|
||||
}
|
||||
|
||||
impl ScimDeleteEvent {
|
||||
pub fn new(ident: Identity, target: Uuid, class: EntryClass) -> Self {
|
||||
ScimDeleteEvent {
|
||||
ident,
|
||||
target,
|
||||
class,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl QueryServerWriteTransaction<'_> {
|
||||
/// SCIM PUT is the handler where a single entry is updated. In a SCIM PUT request
|
||||
/// the request defines the state of an attribute in entirety for the update. This
|
||||
|
@ -115,6 +173,251 @@ impl QueryServerWriteTransaction<'_> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scim_create(
|
||||
&mut self,
|
||||
scim_create: ScimCreateEvent,
|
||||
) -> Result<ScimEntryKanidm, OperationError> {
|
||||
let ScimCreateEvent { ident, entry } = scim_create;
|
||||
|
||||
let create_event = CreateEvent {
|
||||
ident,
|
||||
entries: vec![entry],
|
||||
return_created_uuids: true,
|
||||
};
|
||||
|
||||
let changed_uuids = self.create(&create_event)?;
|
||||
|
||||
let mut changed_uuids = changed_uuids.ok_or(OperationError::SC0028CreatedUuidsInvalid)?;
|
||||
|
||||
let target = if let Some(target) = changed_uuids.pop() {
|
||||
if !changed_uuids.is_empty() {
|
||||
// Too many results!
|
||||
return Err(OperationError::UniqueConstraintViolation);
|
||||
}
|
||||
|
||||
target
|
||||
} else {
|
||||
// No results!
|
||||
return Err(OperationError::NoMatchingEntries);
|
||||
};
|
||||
|
||||
// Now get the entry. We handle a lot of the errors here nicely,
|
||||
// but if we got to this point, they really can't happen.
|
||||
let filter_intent = filter!(f_and!([f_eq(Attribute::Uuid, PartialValue::Uuid(target))]));
|
||||
|
||||
let f_intent_valid = filter_intent
|
||||
.validate(self.get_schema())
|
||||
.map_err(OperationError::SchemaViolation)?;
|
||||
|
||||
let f_valid = f_intent_valid.clone().into_ignore_hidden();
|
||||
|
||||
let se = SearchEvent {
|
||||
ident: create_event.ident,
|
||||
filter: f_valid,
|
||||
filter_orig: f_intent_valid,
|
||||
// Return all attributes
|
||||
attrs: None,
|
||||
effective_access_check: false,
|
||||
};
|
||||
|
||||
let mut vs = self.search_ext(&se)?;
|
||||
match vs.pop() {
|
||||
Some(entry) if vs.is_empty() => entry.to_scim_kanidm(self),
|
||||
_ => {
|
||||
if vs.is_empty() {
|
||||
Err(OperationError::NoMatchingEntries)
|
||||
} else {
|
||||
// Multiple entries matched, should not be possible!
|
||||
Err(OperationError::UniqueConstraintViolation)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scim_delete(&mut self, scim_delete: ScimDeleteEvent) -> Result<(), OperationError> {
|
||||
let ScimDeleteEvent {
|
||||
ident,
|
||||
target,
|
||||
class,
|
||||
} = scim_delete;
|
||||
|
||||
let filter_intent = filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(target)));
|
||||
let f_intent_valid = filter_intent
|
||||
.validate(self.get_schema())
|
||||
.map_err(OperationError::SchemaViolation)?;
|
||||
|
||||
let filter = filter!(f_and!([
|
||||
f_eq(Attribute::Uuid, PartialValue::Uuid(target)),
|
||||
f_eq(Attribute::Class, class.into())
|
||||
]));
|
||||
let f_valid = filter
|
||||
.validate(self.get_schema())
|
||||
.map_err(OperationError::SchemaViolation)?;
|
||||
|
||||
let de = DeleteEvent {
|
||||
ident,
|
||||
filter: f_valid,
|
||||
filter_orig: f_intent_valid,
|
||||
};
|
||||
|
||||
self.delete(&de)
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_scim_json_put(
|
||||
&mut self,
|
||||
attr: &Attribute,
|
||||
value: Option<JsonValue>,
|
||||
) -> Result<Option<ValueSet>, OperationError> {
|
||||
let schema = self.get_schema();
|
||||
// Lookup the attr
|
||||
let Some(schema_a) = schema.get_attributes().get(attr) else {
|
||||
// No attribute of this name exists - fail fast, there is no point to
|
||||
// proceed, as nothing can be satisfied.
|
||||
return Err(OperationError::InvalidAttributeName(attr.to_string()));
|
||||
};
|
||||
|
||||
let Some(value) = value else {
|
||||
// It's a none so the value needs to be unset, and the attr DOES exist in
|
||||
// schema.
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
self.resolve_scim_json(schema_a, value).map(Some)
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_scim_json_post(
|
||||
&mut self,
|
||||
attr: &Attribute,
|
||||
value: JsonValue,
|
||||
) -> Result<ValueSet, OperationError> {
|
||||
let schema = self.get_schema();
|
||||
// Lookup the attr
|
||||
let Some(schema_a) = schema.get_attributes().get(attr) else {
|
||||
// No attribute of this name exists - fail fast, there is no point to
|
||||
// proceed, as nothing can be satisfied.
|
||||
return Err(OperationError::InvalidAttributeName(attr.to_string()));
|
||||
};
|
||||
|
||||
self.resolve_scim_json(schema_a, value)
|
||||
}
|
||||
|
||||
fn resolve_scim_json(
|
||||
&mut self,
|
||||
schema_a: &SchemaAttribute,
|
||||
value: JsonValue,
|
||||
) -> Result<ValueSet, OperationError> {
|
||||
let resolve_status = match schema_a.syntax {
|
||||
SyntaxType::Utf8String => ValueSetUtf8::from_scim_json_put(value),
|
||||
SyntaxType::Utf8StringInsensitive => ValueSetIutf8::from_scim_json_put(value),
|
||||
SyntaxType::Uuid => ValueSetUuid::from_scim_json_put(value),
|
||||
SyntaxType::Boolean => ValueSetBool::from_scim_json_put(value),
|
||||
SyntaxType::SyntaxId => ValueSetSyntax::from_scim_json_put(value),
|
||||
SyntaxType::IndexId => ValueSetIndex::from_scim_json_put(value),
|
||||
SyntaxType::ReferenceUuid => ValueSetRefer::from_scim_json_put(value),
|
||||
SyntaxType::Utf8StringIname => ValueSetIname::from_scim_json_put(value),
|
||||
SyntaxType::NsUniqueId => ValueSetNsUniqueId::from_scim_json_put(value),
|
||||
SyntaxType::DateTime => ValueSetDateTime::from_scim_json_put(value),
|
||||
SyntaxType::EmailAddress => ValueSetEmailAddress::from_scim_json_put(value),
|
||||
SyntaxType::Url => ValueSetUrl::from_scim_json_put(value),
|
||||
SyntaxType::OauthScope => ValueSetOauthScope::from_scim_json_put(value),
|
||||
SyntaxType::OauthScopeMap => ValueSetOauthScopeMap::from_scim_json_put(value),
|
||||
SyntaxType::OauthClaimMap => ValueSetOauthClaimMap::from_scim_json_put(value),
|
||||
SyntaxType::UiHint => ValueSetUiHint::from_scim_json_put(value),
|
||||
SyntaxType::CredentialType => ValueSetCredentialType::from_scim_json_put(value),
|
||||
SyntaxType::Certificate => ValueSetCertificate::from_scim_json_put(value),
|
||||
SyntaxType::SshKey => ValueSetSshKey::from_scim_json_put(value),
|
||||
SyntaxType::Uint32 => ValueSetUint32::from_scim_json_put(value),
|
||||
|
||||
// Not Yet ... if ever
|
||||
// SyntaxType::JsonFilter => ValueSetJsonFilter::from_scim_json_put(value),
|
||||
SyntaxType::JsonFilter => Err(OperationError::InvalidAttribute(
|
||||
"Json Filters are not able to be set.".to_string(),
|
||||
)),
|
||||
// Can't be set currently as these are only internally generated for key-id's
|
||||
// SyntaxType::HexString => ValueSetHexString::from_scim_json_put(value),
|
||||
SyntaxType::HexString => Err(OperationError::InvalidAttribute(
|
||||
"Hex strings are not able to be set.".to_string(),
|
||||
)),
|
||||
|
||||
// Can't be set until we have better error handling in the set paths
|
||||
// SyntaxType::Image => ValueSetImage::from_scim_json_put(value),
|
||||
SyntaxType::Image => Err(OperationError::InvalidAttribute(
|
||||
"Images are not able to be set.".to_string(),
|
||||
)),
|
||||
|
||||
// Can't be set yet, mostly as I'm lazy
|
||||
// SyntaxType::WebauthnAttestationCaList => {
|
||||
// ValueSetWebauthnAttestationCaList::from_scim_json_put(value)
|
||||
// }
|
||||
SyntaxType::WebauthnAttestationCaList => Err(OperationError::InvalidAttribute(
|
||||
"Webauthn Attestation Ca Lists are not able to be set.".to_string(),
|
||||
)),
|
||||
|
||||
// Syntax types that can not be submitted
|
||||
SyntaxType::Credential => Err(OperationError::InvalidAttribute(
|
||||
"Credentials are not able to be set.".to_string(),
|
||||
)),
|
||||
SyntaxType::SecretUtf8String => Err(OperationError::InvalidAttribute(
|
||||
"Secrets are not able to be set.".to_string(),
|
||||
)),
|
||||
SyntaxType::SecurityPrincipalName => Err(OperationError::InvalidAttribute(
|
||||
"SPNs are not able to be set.".to_string(),
|
||||
)),
|
||||
SyntaxType::Cid => Err(OperationError::InvalidAttribute(
|
||||
"CIDs are not able to be set.".to_string(),
|
||||
)),
|
||||
SyntaxType::PrivateBinary => Err(OperationError::InvalidAttribute(
|
||||
"Private Binaries are not able to be set.".to_string(),
|
||||
)),
|
||||
SyntaxType::IntentToken => Err(OperationError::InvalidAttribute(
|
||||
"Intent Tokens are not able to be set.".to_string(),
|
||||
)),
|
||||
SyntaxType::Passkey => Err(OperationError::InvalidAttribute(
|
||||
"Passkeys are not able to be set.".to_string(),
|
||||
)),
|
||||
SyntaxType::AttestedPasskey => Err(OperationError::InvalidAttribute(
|
||||
"Attested Passkeys are not able to be set.".to_string(),
|
||||
)),
|
||||
SyntaxType::Session => Err(OperationError::InvalidAttribute(
|
||||
"Sessions are not able to be set.".to_string(),
|
||||
)),
|
||||
SyntaxType::JwsKeyEs256 => Err(OperationError::InvalidAttribute(
|
||||
"Jws ES256 Private Keys are not able to be set.".to_string(),
|
||||
)),
|
||||
SyntaxType::JwsKeyRs256 => Err(OperationError::InvalidAttribute(
|
||||
"Jws RS256 Private Keys are not able to be set.".to_string(),
|
||||
)),
|
||||
SyntaxType::Oauth2Session => Err(OperationError::InvalidAttribute(
|
||||
"Sessions are not able to be set.".to_string(),
|
||||
)),
|
||||
SyntaxType::TotpSecret => Err(OperationError::InvalidAttribute(
|
||||
"TOTP Secrets are not able to be set.".to_string(),
|
||||
)),
|
||||
SyntaxType::ApiToken => Err(OperationError::InvalidAttribute(
|
||||
"API Tokens are not able to be set.".to_string(),
|
||||
)),
|
||||
SyntaxType::AuditLogString => Err(OperationError::InvalidAttribute(
|
||||
"Audit Strings are not able to be set.".to_string(),
|
||||
)),
|
||||
SyntaxType::EcKeyPrivate => Err(OperationError::InvalidAttribute(
|
||||
"EC Private Keys are not able to be set.".to_string(),
|
||||
)),
|
||||
SyntaxType::KeyInternal => Err(OperationError::InvalidAttribute(
|
||||
"Key Internal Structures are not able to be set.".to_string(),
|
||||
)),
|
||||
SyntaxType::ApplicationPassword => Err(OperationError::InvalidAttribute(
|
||||
"Application Passwords are not able to be set.".to_string(),
|
||||
)),
|
||||
}?;
|
||||
|
||||
match resolve_status {
|
||||
ValueSetResolveStatus::Resolved(vs) => Ok(vs),
|
||||
ValueSetResolveStatus::NeedsResolution(vs_inter) => {
|
||||
self.resolve_valueset_intermediate(vs_inter)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -186,7 +489,7 @@ mod tests {
|
|||
|
||||
match desc {
|
||||
ScimValueKanidm::String(gdesc) if gdesc == "Group Description" => {}
|
||||
_ => assert!(false),
|
||||
_ => unreachable!("Expected a string"),
|
||||
};
|
||||
|
||||
// null removes attr
|
||||
|
@ -201,7 +504,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 +537,7 @@ mod tests {
|
|||
value: "extra_1@example.com".to_string(),
|
||||
}));
|
||||
}
|
||||
_ => assert!(false),
|
||||
_ => unreachable!("Expected 1 member"),
|
||||
};
|
||||
|
||||
// set many
|
||||
|
@ -285,7 +588,7 @@ mod tests {
|
|||
value: "extra_3@example.com".to_string(),
|
||||
}));
|
||||
}
|
||||
_ => assert!(false),
|
||||
_ => unreachable!("Expected 3 members"),
|
||||
};
|
||||
|
||||
// set many with a removal
|
||||
|
@ -333,7 +636,7 @@ mod tests {
|
|||
value: "extra_2@example.com".to_string(),
|
||||
}));
|
||||
}
|
||||
_ => assert!(false),
|
||||
_ => unreachable!("Expected 2 members"),
|
||||
};
|
||||
|
||||
// empty set removes attr
|
||||
|
@ -348,6 +651,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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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, &[])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -381,6 +381,6 @@ mod tests {
|
|||
}
|
||||
]
|
||||
"#;
|
||||
crate::valueset::scim_json_reflexive(vs, data);
|
||||
crate::valueset::scim_json_reflexive(&vs, data);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -400,6 +400,6 @@ mod tests {
|
|||
}
|
||||
]
|
||||
"#;
|
||||
crate::valueset::scim_json_reflexive(vs, data);
|
||||
crate::valueset::scim_json_reflexive(&vs, data);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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, &[])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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, &[])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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, &[])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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, &[])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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""#);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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!(
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
|
|
@ -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, &[])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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, &[])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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, &[])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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, &[])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -649,6 +649,6 @@ mod tests {
|
|||
}
|
||||
]
|
||||
"#;
|
||||
crate::valueset::scim_json_reflexive(vs, data);
|
||||
crate::valueset::scim_json_reflexive(&vs, data);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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, &[])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
&[],
|
||||
);
|
||||
|
||||
|
|
|
@ -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""#);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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""#);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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, &[])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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, &[])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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, &[])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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, &[])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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, &[])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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, &[])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -238,6 +238,13 @@ impl ValueSetScimPut for ValueSetRefer {
|
|||
fn from_scim_json_put(value: JsonValue) -> Result<ValueSetResolveStatus, OperationError> {
|
||||
use kanidm_proto::scim_v1::client::{ScimReference, ScimReferences};
|
||||
|
||||
// May be a single reference, lets wrap it in an array to proceed.
|
||||
let value = if !value.is_array() && value.is_object() {
|
||||
JsonValue::Array(vec![value])
|
||||
} else {
|
||||
value
|
||||
};
|
||||
|
||||
let scim_refs: ScimReferences = serde_json::from_value(value).map_err(|err| {
|
||||
warn!(?err, "Invalid SCIM reference set syntax");
|
||||
OperationError::SC0002ReferenceSyntaxInvalid
|
||||
|
@ -422,10 +429,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 +456,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,
|
||||
&[],
|
||||
);
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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"]);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
324
server/testkit/tests/testkit/ip_addr_extractors.rs
Normal file
324
server/testkit/tests/testkit/ip_addr_extractors.rs
Normal 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);
|
||||
}
|
|
@ -1,5 +1,10 @@
|
|||
use kanidmd_testkit::AsyncTestEnvironment;
|
||||
use kanidm_proto::scim_v1::client::{ScimEntryApplicationPost, ScimReference};
|
||||
use kanidmd_testkit::{AsyncTestEnvironment, IDM_ADMIN_TEST_PASSWORD, IDM_ADMIN_TEST_USER};
|
||||
use ldap3_client::LdapClientBuilder;
|
||||
use tracing::debug;
|
||||
|
||||
const TEST_PERSON: &str = "user_mcuserton";
|
||||
const TEST_GROUP: &str = "group_mcgroupington";
|
||||
|
||||
#[kanidmd_testkit::test(ldap = true)]
|
||||
async fn test_ldap_basic_unix_bind(test_env: &AsyncTestEnvironment) {
|
||||
|
@ -14,3 +19,75 @@ async fn test_ldap_basic_unix_bind(test_env: &AsyncTestEnvironment) {
|
|||
|
||||
assert_eq!(whoami, Some("u: anonymous@localhost".to_string()));
|
||||
}
|
||||
|
||||
#[kanidmd_testkit::test(ldap = true)]
|
||||
async fn test_ldap_application_password_basic(test_env: &AsyncTestEnvironment) {
|
||||
const APPLICATION_1_NAME: &str = "test_application_1";
|
||||
|
||||
// Remember, this isn't the exhaustive test for application password behaviours,
|
||||
// those are in the main server. This is just a basic smoke test that the interfaces
|
||||
// are exposed and work in a basic manner.
|
||||
|
||||
let idm_admin_rsclient = test_env.rsclient.new_session().unwrap();
|
||||
|
||||
// Create a person
|
||||
|
||||
idm_admin_rsclient
|
||||
.auth_simple_password(IDM_ADMIN_TEST_USER, IDM_ADMIN_TEST_PASSWORD)
|
||||
.await
|
||||
.expect("Failed to login as admin");
|
||||
|
||||
idm_admin_rsclient
|
||||
.idm_person_account_create(TEST_PERSON, TEST_PERSON)
|
||||
.await
|
||||
.expect("Failed to create the user");
|
||||
|
||||
idm_admin_rsclient
|
||||
.idm_group_create(TEST_GROUP, None)
|
||||
.await
|
||||
.expect("Failed to create test group");
|
||||
|
||||
// Create two applications
|
||||
|
||||
let application_1 = ScimEntryApplicationPost {
|
||||
name: APPLICATION_1_NAME.to_string(),
|
||||
displayname: APPLICATION_1_NAME.to_string(),
|
||||
linked_group: ScimReference::from(TEST_GROUP),
|
||||
};
|
||||
|
||||
let application_entry = idm_admin_rsclient
|
||||
.idm_application_create(&application_1)
|
||||
.await
|
||||
.expect("Failed to create the user");
|
||||
|
||||
debug!(?application_entry);
|
||||
|
||||
// List, get them.
|
||||
|
||||
// Login as the person
|
||||
|
||||
// Create application passwords
|
||||
|
||||
// Check the work.
|
||||
|
||||
// Check they can't cross talk.
|
||||
|
||||
// Done!
|
||||
|
||||
// let ldap_url = test_env.ldap_url.as_ref().unwrap();
|
||||
|
||||
// let mut ldap_client = LdapClientBuilder::new(ldap_url).build().await.unwrap();
|
||||
|
||||
let result = idm_admin_rsclient
|
||||
.idm_application_delete(APPLICATION_1_NAME)
|
||||
.await
|
||||
.expect("Failed to create the user");
|
||||
|
||||
debug!(?result);
|
||||
|
||||
// Delete the applications
|
||||
|
||||
// Check that you can no longer bind.
|
||||
|
||||
// They no longer list
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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<_>>();
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue