mirror of
https://github.com/kanidm/kanidm.git
synced 2025-05-02 07:05:06 +02:00
2763 lines
103 KiB
Rust
2763 lines
103 KiB
Rust
//! LDAP specific operations handling components. This is where LDAP operations
|
|
//! are sent to for processing.
|
|
|
|
use std::collections::BTreeSet;
|
|
use std::iter;
|
|
use std::str::FromStr;
|
|
|
|
use compact_jwt::JwsCompact;
|
|
use kanidm_proto::constants::*;
|
|
use kanidm_proto::internal::{ApiToken, UserAuthToken};
|
|
use ldap3_proto::simple::*;
|
|
use regex::{Regex, RegexBuilder};
|
|
use std::net::IpAddr;
|
|
use tracing::trace;
|
|
use uuid::Uuid;
|
|
|
|
use crate::event::SearchEvent;
|
|
use crate::idm::event::{LdapApplicationAuthEvent, LdapAuthEvent, LdapTokenAuthEvent};
|
|
use crate::idm::server::{IdmServer, IdmServerAuthTransaction, IdmServerTransaction};
|
|
use crate::prelude::*;
|
|
|
|
// Clippy doesn't like Bind here. But proto needs unboxed ldapmsg,
|
|
// and ldapboundtoken is moved. Really, it's not too bad, every message here is pretty sucky.
|
|
#[allow(clippy::large_enum_variant)]
|
|
pub enum LdapResponseState {
|
|
Unbind,
|
|
Disconnect(LdapMsg),
|
|
Bind(LdapBoundToken, LdapMsg),
|
|
Respond(LdapMsg),
|
|
MultiPartResponse(Vec<LdapMsg>),
|
|
BindMultiPartResponse(LdapBoundToken, Vec<LdapMsg>),
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum LdapSession {
|
|
// Maps through and provides anon read, but allows us to check the validity
|
|
// of the account still.
|
|
UnixBind(Uuid),
|
|
UserAuthToken(UserAuthToken),
|
|
ApiToken(ApiToken),
|
|
ApplicationPasswordBind(Uuid, Uuid),
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct LdapBoundToken {
|
|
// Used to help ID the user doing the action, makes logging nicer.
|
|
pub spn: String,
|
|
pub session_id: Uuid,
|
|
// This is the effective session permission. This is generated from either:
|
|
// * A valid anonymous bind
|
|
// * A valid unix pw bind
|
|
// * A valid ApiToken
|
|
// In a way, this is a stepping stone to an "ident" but allows us to check
|
|
// the session is still "valid" depending on it's origin.
|
|
pub effective_session: LdapSession,
|
|
}
|
|
|
|
pub struct LdapServer {
|
|
rootdse: LdapSearchResultEntry,
|
|
basedn: String,
|
|
dnre: Regex,
|
|
binddnre: Regex,
|
|
max_queryable_attrs: usize,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
enum LdapBindTarget {
|
|
Account(Uuid),
|
|
ApiToken,
|
|
Application(String, Uuid),
|
|
}
|
|
|
|
impl LdapServer {
|
|
pub async fn new(idms: &IdmServer) -> Result<Self, OperationError> {
|
|
// let ct = duration_from_epoch_now();
|
|
let mut idms_prox_read = idms.proxy_read().await?;
|
|
// This is the rootdse path.
|
|
// get the domain_info item
|
|
let domain_entry = idms_prox_read
|
|
.qs_read
|
|
.internal_search_uuid(UUID_DOMAIN_INFO)?;
|
|
|
|
// Get the maximum number of queryable attributes from the domain entry
|
|
let max_queryable_attrs = domain_entry
|
|
.get_ava_single_uint32(Attribute::LdapMaxQueryableAttrs)
|
|
.map(|u| u as usize)
|
|
.unwrap_or(DEFAULT_LDAP_MAXIMUM_QUERYABLE_ATTRIBUTES);
|
|
|
|
let basedn = domain_entry
|
|
.get_ava_single_iutf8(Attribute::DomainLdapBasedn)
|
|
.map(|s| s.to_string())
|
|
.or_else(|| {
|
|
domain_entry
|
|
.get_ava_single_iname(Attribute::DomainName)
|
|
.map(ldap_domain_to_dc)
|
|
})
|
|
.ok_or(OperationError::InvalidEntryState)?;
|
|
|
|
// It is necessary to swap greed to avoid the first group "<attr>=<val>" matching the
|
|
// next group "app=<app>", son one can use "app=app1,dc=test,dc=net" as search base:
|
|
// Greedy (app=app1,dc=test,dc=net):
|
|
// Match 1 - app=app1,dc=test,dc=net
|
|
// Group 1 - app=app1,
|
|
// Group <attr> - app
|
|
// Group <val> - app1
|
|
// Group 6 - dc=test,dc=net
|
|
// Ungreedy (app=app1,dc=test,dc=net):
|
|
// Match 1 - app=app1,dc=test,dc=net
|
|
// Group 4 - app=app1,
|
|
// Group <app> - app1
|
|
// Group 6 - dc=test,dc=net
|
|
let dnre = RegexBuilder::new(
|
|
format!("^((?P<attr>[^=,]+)=(?P<val>[^=,]+),)?(app=(?P<app>[^=,]+),)?({basedn})$")
|
|
.as_str(),
|
|
)
|
|
.swap_greed(true)
|
|
.build()
|
|
.map_err(|_| OperationError::InvalidEntryState)?;
|
|
|
|
let binddnre = Regex::new(
|
|
format!("^((([^=,]+)=)?(?P<val>[^=,]+))(,app=(?P<app>[^=,]+))?(,{basedn})?$").as_str(),
|
|
)
|
|
.map_err(|_| OperationError::InvalidEntryState)?;
|
|
|
|
let rootdse = LdapSearchResultEntry {
|
|
dn: "".to_string(),
|
|
attributes: vec![
|
|
LdapPartialAttribute {
|
|
atype: ATTR_OBJECTCLASS.to_string(),
|
|
vals: vec!["top".as_bytes().to_vec()],
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "vendorname".to_string(),
|
|
vals: vec!["Kanidm Project".as_bytes().to_vec()],
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "vendorversion".to_string(),
|
|
vals: vec![env!("CARGO_PKG_VERSION").as_bytes().to_vec()],
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "supportedldapversion".to_string(),
|
|
vals: vec!["3".as_bytes().to_vec()],
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "supportedextension".to_string(),
|
|
vals: vec!["1.3.6.1.4.1.4203.1.11.3".as_bytes().to_vec()],
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "supportedfeatures".to_string(),
|
|
vals: vec!["1.3.6.1.4.1.4203.1.5.1".as_bytes().to_vec()],
|
|
},
|
|
LdapPartialAttribute {
|
|
atype: "defaultnamingcontext".to_string(),
|
|
vals: vec![basedn.as_bytes().to_vec()],
|
|
},
|
|
],
|
|
};
|
|
|
|
Ok(LdapServer {
|
|
rootdse,
|
|
basedn,
|
|
dnre,
|
|
binddnre,
|
|
max_queryable_attrs,
|
|
})
|
|
}
|
|
|
|
#[instrument(level = "debug", skip_all)]
|
|
async fn do_search(
|
|
&self,
|
|
idms: &IdmServer,
|
|
sr: &SearchRequest,
|
|
uat: &LdapBoundToken,
|
|
source: Source,
|
|
// eventid: &Uuid,
|
|
) -> Result<Vec<LdapMsg>, OperationError> {
|
|
admin_info!("Attempt LDAP Search for {}", uat.spn);
|
|
// If the request is "", Base, Present(Attribute::ObjectClass.into()), [], then we want the rootdse.
|
|
if sr.base.is_empty() && sr.scope == LdapSearchScope::Base {
|
|
admin_info!("LDAP Search success - RootDSE");
|
|
Ok(vec![
|
|
sr.gen_result_entry(self.rootdse.clone()),
|
|
sr.gen_success(),
|
|
])
|
|
} else {
|
|
// We want something else apparently. Need to do some more work ...
|
|
// Parse the operation and make sure it's sane before we start the txn.
|
|
|
|
// This scoping returns an extra filter component.
|
|
|
|
let (opt_attr, opt_value) = match self.dnre.captures(sr.base.as_str()) {
|
|
Some(caps) => (
|
|
caps.name("attr").map(|v| v.as_str().to_string()),
|
|
caps.name("val").map(|v| v.as_str().to_string()),
|
|
),
|
|
None => {
|
|
request_error!("LDAP Search failure - invalid basedn");
|
|
return Err(OperationError::InvalidRequestState);
|
|
}
|
|
};
|
|
|
|
let req_dn = match (opt_attr, opt_value) {
|
|
(Some(a), Some(v)) => Some((a, v)),
|
|
(None, None) => None,
|
|
_ => {
|
|
request_error!("LDAP Search failure - invalid rdn");
|
|
return Err(OperationError::InvalidRequestState);
|
|
}
|
|
};
|
|
|
|
trace!(rdn = ?req_dn);
|
|
|
|
// Map the Some(a,v) to ...?
|
|
|
|
let ext_filter = match (&sr.scope, req_dn) {
|
|
// OneLevel and Child searches are **very** similar for us because child
|
|
// is a "subtree search excluding base". Because we don't have a tree structure at
|
|
// all, this is the same as a one level (all children of base excluding base).
|
|
(LdapSearchScope::Children, Some(_r)) | (LdapSearchScope::OneLevel, Some(_r)) => {
|
|
return Ok(vec![sr.gen_success()])
|
|
}
|
|
(LdapSearchScope::Children, None) | (LdapSearchScope::OneLevel, None) => {
|
|
// exclude domain_info
|
|
Some(LdapFilter::Not(Box::new(LdapFilter::Equality(
|
|
Attribute::Uuid.to_string(),
|
|
STR_UUID_DOMAIN_INFO.to_string(),
|
|
))))
|
|
}
|
|
// because we request a specific DN, these are the same since we want the same
|
|
// entry.
|
|
(LdapSearchScope::Base, Some((a, v)))
|
|
| (LdapSearchScope::Subtree, Some((a, v))) => Some(LdapFilter::Equality(a, v)),
|
|
(LdapSearchScope::Base, None) => {
|
|
// domain_info
|
|
Some(LdapFilter::Equality(
|
|
Attribute::Uuid.to_string(),
|
|
STR_UUID_DOMAIN_INFO.to_string(),
|
|
))
|
|
}
|
|
(LdapSearchScope::Subtree, None) => {
|
|
// No filter changes needed.
|
|
None
|
|
}
|
|
};
|
|
|
|
let mut no_attrs = false;
|
|
let mut all_attrs = false;
|
|
let mut all_op_attrs = false;
|
|
|
|
let attrs_len = sr.attrs.len();
|
|
if sr.attrs.is_empty() {
|
|
// If [], then "all" attrs
|
|
all_attrs = true;
|
|
} else if attrs_len < self.max_queryable_attrs {
|
|
sr.attrs.iter().for_each(|a| {
|
|
if a == "*" {
|
|
all_attrs = true;
|
|
} else if a == "+" {
|
|
// This forces the BE to get all the attrs so we can
|
|
// map all vattrs.
|
|
all_attrs = true;
|
|
all_op_attrs = true;
|
|
} else if a == "1.1" {
|
|
/*
|
|
* ref https://www.rfc-editor.org/rfc/rfc4511#section-4.5.1.8
|
|
*
|
|
* A list containing only the OID "1.1" indicates that no
|
|
* attributes are to be returned. If "1.1" is provided with other
|
|
* attributeSelector values, the "1.1" attributeSelector is
|
|
* ignored. This OID was chosen because it does not (and can not)
|
|
* correspond to any attribute in use.
|
|
*/
|
|
if sr.attrs.len() == 1 {
|
|
no_attrs = true;
|
|
}
|
|
}
|
|
})
|
|
} else {
|
|
admin_error!(
|
|
"Too many LDAP attributes requested. Maximum allowed is {}, while your search query had {}",
|
|
self.max_queryable_attrs, attrs_len
|
|
);
|
|
return Err(OperationError::ResourceLimit);
|
|
}
|
|
|
|
// We need to retain this to know what the client requested.
|
|
let (k_attrs, l_attrs) = if no_attrs {
|
|
// Request no attributes and no mapped attributes.
|
|
(None, Vec::with_capacity(0))
|
|
} else if all_op_attrs {
|
|
// We need all attrs, and we do a full v_attr map.
|
|
(None, ldap_all_vattrs())
|
|
} else if all_attrs {
|
|
// We are already getting all attrs, but if there are any virtual attrs
|
|
// we need them in our request as well.
|
|
let req_attrs: Vec<String> = sr
|
|
.attrs
|
|
.iter()
|
|
.filter_map(|a| {
|
|
let a_lower = a.to_lowercase();
|
|
|
|
if ldap_vattr_map(&a_lower).is_some() {
|
|
Some(a_lower)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
(None, req_attrs)
|
|
} else {
|
|
// What the client requested, in LDAP forms.
|
|
let req_attrs: Vec<String> = sr
|
|
.attrs
|
|
.iter()
|
|
.filter_map(|a| {
|
|
if a == "*" || a == "+" || a == "1.1" {
|
|
None
|
|
} else {
|
|
Some(a.to_lowercase())
|
|
}
|
|
})
|
|
.collect();
|
|
// This is what the client requested, but mapped to kanidm forms.
|
|
// NOTE: All req_attrs are lowercase at this point.
|
|
let mapped_attrs: BTreeSet<_> = req_attrs
|
|
.iter()
|
|
.map(|a| Attribute::from(ldap_vattr_map(a).unwrap_or(a)))
|
|
.collect();
|
|
|
|
(Some(mapped_attrs), req_attrs)
|
|
};
|
|
|
|
admin_info!(attr = ?l_attrs, "LDAP Search Request LDAP Attrs");
|
|
admin_info!(attr = ?k_attrs, "LDAP Search Request Mapped Attrs");
|
|
|
|
let ct = duration_from_epoch_now();
|
|
let mut idm_read = idms.proxy_read().await?;
|
|
// Now start the txn - we need it for resolving filter components.
|
|
|
|
// join the filter, with ext_filter
|
|
let lfilter = match ext_filter {
|
|
Some(ext) => LdapFilter::And(vec![
|
|
sr.filter.clone(),
|
|
ext,
|
|
LdapFilter::Not(Box::new(LdapFilter::Or(vec![
|
|
LdapFilter::Equality(Attribute::Class.to_string(), "classtype".to_string()),
|
|
LdapFilter::Equality(
|
|
Attribute::Class.to_string(),
|
|
"attributetype".to_string(),
|
|
),
|
|
LdapFilter::Equality(
|
|
Attribute::Class.to_string(),
|
|
"access_control_profile".to_string(),
|
|
),
|
|
]))),
|
|
]),
|
|
None => LdapFilter::And(vec![
|
|
sr.filter.clone(),
|
|
LdapFilter::Not(Box::new(LdapFilter::Or(vec![
|
|
LdapFilter::Equality(Attribute::Class.to_string(), "classtype".to_string()),
|
|
LdapFilter::Equality(
|
|
Attribute::Class.to_string(),
|
|
"attributetype".to_string(),
|
|
),
|
|
LdapFilter::Equality(
|
|
Attribute::Class.to_string(),
|
|
"access_control_profile".to_string(),
|
|
),
|
|
]))),
|
|
]),
|
|
};
|
|
|
|
admin_info!(filter = ?lfilter, "LDAP Search Filter");
|
|
|
|
// Build the event, with the permissions from effective_session
|
|
//
|
|
// ! Remember, searchEvent wraps to ignore hidden for us.
|
|
let ident = idm_read
|
|
.validate_ldap_session(&uat.effective_session, source, ct)
|
|
.map_err(|e| {
|
|
admin_error!("Invalid identity: {:?}", e);
|
|
e
|
|
})?;
|
|
let se = SearchEvent::new_ext_impersonate_uuid(
|
|
&mut idm_read.qs_read,
|
|
ident,
|
|
&lfilter,
|
|
k_attrs,
|
|
)
|
|
.map_err(|e| {
|
|
admin_error!("failed to create search event -> {:?}", e);
|
|
e
|
|
})?;
|
|
|
|
let res = idm_read.qs_read.search_ext(&se).map_err(|e| {
|
|
admin_error!("search failure {:?}", e);
|
|
e
|
|
})?;
|
|
|
|
// These have already been fully reduced (access controls applied),
|
|
// so we can just transform the values and open palm slam them into
|
|
// the result structure.
|
|
let lres: Result<Vec<_>, _> = res
|
|
.into_iter()
|
|
.map(|e| {
|
|
e.to_ldap(
|
|
&mut idm_read.qs_read,
|
|
self.basedn.as_str(),
|
|
all_attrs,
|
|
&l_attrs,
|
|
)
|
|
// if okay, wrap in a ldap msg.
|
|
.map(|r| sr.gen_result_entry(r))
|
|
})
|
|
.chain(iter::once(Ok(sr.gen_success())))
|
|
.collect();
|
|
|
|
let lres = lres.map_err(|e| {
|
|
admin_error!("entry resolve failure {:?}", e);
|
|
e
|
|
})?;
|
|
|
|
admin_info!(
|
|
nentries = %lres.len(),
|
|
"LDAP Search Success -> number of entries"
|
|
);
|
|
|
|
Ok(lres)
|
|
}
|
|
}
|
|
|
|
async fn do_bind(
|
|
&self,
|
|
idms: &IdmServer,
|
|
dn: &str,
|
|
pw: &str,
|
|
) -> Result<Option<LdapBoundToken>, OperationError> {
|
|
security_info!(
|
|
"Attempt LDAP Bind for {}",
|
|
if dn.is_empty() { "(empty dn)" } else { dn }
|
|
);
|
|
let ct = duration_from_epoch_now();
|
|
|
|
let mut idm_auth = idms.auth().await?;
|
|
let target = self.bind_target_from_bind_dn(&mut idm_auth, dn, pw).await?;
|
|
|
|
let result = match target {
|
|
LdapBindTarget::Account(uuid) => {
|
|
let lae = LdapAuthEvent::from_parts(uuid, pw.to_string())?;
|
|
idm_auth.auth_ldap(&lae, ct).await?
|
|
}
|
|
LdapBindTarget::ApiToken => {
|
|
let jwsc = JwsCompact::from_str(pw).map_err(|err| {
|
|
error!(?err, "Invalid JwsCompact supplied as authentication token.");
|
|
OperationError::NotAuthenticated
|
|
})?;
|
|
|
|
let lae = LdapTokenAuthEvent::from_parts(jwsc)?;
|
|
idm_auth.token_auth_ldap(&lae, ct).await?
|
|
}
|
|
LdapBindTarget::Application(ref app_name, usr_uuid) => {
|
|
let lae =
|
|
LdapApplicationAuthEvent::new(app_name.as_str(), usr_uuid, pw.to_string())?;
|
|
idm_auth.application_auth_ldap(&lae, ct).await?
|
|
}
|
|
};
|
|
|
|
idm_auth.commit()?;
|
|
|
|
if result.is_some() {
|
|
security_info!(
|
|
"✅ LDAP Bind success for {} -> {:?}",
|
|
if dn.is_empty() { "(empty dn)" } else { dn },
|
|
target
|
|
);
|
|
} else {
|
|
security_info!(
|
|
"❌ LDAP Bind failure for {} -> {:?}",
|
|
if dn.is_empty() { "(empty dn)" } else { dn },
|
|
target
|
|
);
|
|
}
|
|
|
|
Ok(result)
|
|
}
|
|
|
|
#[instrument(level = "debug", skip_all)]
|
|
async fn do_compare(
|
|
&self,
|
|
idms: &IdmServer,
|
|
cr: &CompareRequest,
|
|
uat: &LdapBoundToken,
|
|
source: Source,
|
|
) -> Result<Vec<LdapMsg>, OperationError> {
|
|
admin_info!("Attempt LDAP CompareRequest for {}", uat.spn);
|
|
|
|
let (opt_attr, opt_value) = match self.dnre.captures(cr.entry.as_str()) {
|
|
Some(caps) => (
|
|
caps.name("attr").map(|v| v.as_str().to_string()),
|
|
caps.name("val").map(|v| v.as_str().to_string()),
|
|
),
|
|
None => {
|
|
request_error!("LDAP Search failure - invalid basedn");
|
|
return Err(OperationError::InvalidRequestState);
|
|
}
|
|
};
|
|
|
|
let ext_filter = match (opt_attr, opt_value) {
|
|
(Some(a), Some(v)) => LdapFilter::Equality(a, v),
|
|
_ => {
|
|
request_error!("LDAP Search failure - invalid rdn");
|
|
return Err(OperationError::InvalidRequestState);
|
|
}
|
|
};
|
|
|
|
let ct = duration_from_epoch_now();
|
|
let mut idm_read = idms.proxy_read().await?;
|
|
// Now start the txn - we need it for resolving filter components.
|
|
|
|
// join the filter, with ext_filter
|
|
let lfilter = LdapFilter::And(vec![
|
|
ext_filter.clone(),
|
|
LdapFilter::Equality(cr.atype.clone(), cr.val.clone()),
|
|
LdapFilter::Not(Box::new(LdapFilter::Or(vec![
|
|
LdapFilter::Equality(Attribute::Class.to_string(), "classtype".to_string()),
|
|
LdapFilter::Equality(Attribute::Class.to_string(), "attributetype".to_string()),
|
|
LdapFilter::Equality(
|
|
Attribute::Class.to_string(),
|
|
"access_control_profile".to_string(),
|
|
),
|
|
]))),
|
|
]);
|
|
|
|
admin_info!(filter = ?lfilter, "LDAP Compare Filter");
|
|
|
|
// Build the event, with the permissions from effective_session
|
|
let ident = idm_read
|
|
.validate_ldap_session(&uat.effective_session, source, ct)
|
|
.map_err(|e| {
|
|
admin_error!("Invalid identity: {:?}", e);
|
|
e
|
|
})?;
|
|
|
|
let f = Filter::from_ldap_ro(&ident, &lfilter, &mut idm_read.qs_read)?;
|
|
let filter_orig = f
|
|
.validate(idm_read.qs_read.get_schema())
|
|
.map_err(OperationError::SchemaViolation)?;
|
|
let filter = filter_orig.clone().into_ignore_hidden();
|
|
|
|
let ee = ExistsEvent {
|
|
ident: ident.clone(),
|
|
filter,
|
|
filter_orig,
|
|
};
|
|
|
|
let res = idm_read.qs_read.exists(&ee).map_err(|e| {
|
|
admin_error!("call to exists failure {:?}", e);
|
|
e
|
|
})?;
|
|
|
|
if res {
|
|
admin_info!("LDAP Compare -> True");
|
|
return Ok(vec![cr.gen_compare_true()]);
|
|
}
|
|
|
|
// we need to check if the entry exists at all (without the ava).
|
|
let lfilter = LdapFilter::And(vec![
|
|
ext_filter,
|
|
LdapFilter::Not(Box::new(LdapFilter::Or(vec![
|
|
LdapFilter::Equality(Attribute::Class.to_string(), "classtype".to_string()),
|
|
LdapFilter::Equality(Attribute::Class.to_string(), "attributetype".to_string()),
|
|
LdapFilter::Equality(
|
|
Attribute::Class.to_string(),
|
|
"access_control_profile".to_string(),
|
|
),
|
|
]))),
|
|
]);
|
|
let f = Filter::from_ldap_ro(&ident, &lfilter, &mut idm_read.qs_read)?;
|
|
let filter_orig = f
|
|
.validate(idm_read.qs_read.get_schema())
|
|
.map_err(OperationError::SchemaViolation)?;
|
|
let filter = filter_orig.clone().into_ignore_hidden();
|
|
let ee = ExistsEvent {
|
|
ident,
|
|
filter,
|
|
filter_orig,
|
|
};
|
|
|
|
let res = idm_read.qs_read.exists(&ee).map_err(|e| {
|
|
admin_error!("call to exists failure {:?}", e);
|
|
e
|
|
})?;
|
|
|
|
if res {
|
|
admin_info!("LDAP Compare -> False");
|
|
return Ok(vec![cr.gen_compare_false()]);
|
|
}
|
|
|
|
Ok(vec![
|
|
cr.gen_error(LdapResultCode::NoSuchObject, "".to_string())
|
|
])
|
|
}
|
|
|
|
pub async fn do_op(
|
|
&self,
|
|
idms: &IdmServer,
|
|
server_op: ServerOps,
|
|
uat: Option<LdapBoundToken>,
|
|
ip_addr: IpAddr,
|
|
eventid: Uuid,
|
|
) -> Result<LdapResponseState, OperationError> {
|
|
let source = Source::Ldaps(ip_addr);
|
|
|
|
match server_op {
|
|
ServerOps::SimpleBind(sbr) => self
|
|
.do_bind(idms, sbr.dn.as_str(), sbr.pw.as_str())
|
|
.await
|
|
.map(|r| match r {
|
|
Some(lbt) => LdapResponseState::Bind(lbt, sbr.gen_success()),
|
|
None => LdapResponseState::Respond(sbr.gen_invalid_cred()),
|
|
})
|
|
.or_else(|e| {
|
|
let (rc, msg) = operationerr_to_ldapresultcode(e);
|
|
Ok(LdapResponseState::Respond(sbr.gen_error(rc, msg)))
|
|
}),
|
|
ServerOps::Search(sr) => match uat {
|
|
Some(u) => self
|
|
.do_search(idms, &sr, &u, source)
|
|
.await
|
|
.map(LdapResponseState::MultiPartResponse)
|
|
.or_else(|e| {
|
|
let (rc, msg) = operationerr_to_ldapresultcode(e);
|
|
Ok(LdapResponseState::Respond(sr.gen_error(rc, msg)))
|
|
}),
|
|
None => {
|
|
// Search can occur without a bind, so bind first.
|
|
// This is per section 4 of RFC 4513 (https://www.rfc-editor.org/rfc/rfc4513#section-4).
|
|
let lbt = match self.do_bind(idms, "", "").await {
|
|
Ok(Some(lbt)) => lbt,
|
|
Ok(None) => {
|
|
return Ok(LdapResponseState::Respond(
|
|
sr.gen_error(LdapResultCode::InvalidCredentials, "".to_string()),
|
|
))
|
|
}
|
|
Err(e) => {
|
|
let (rc, msg) = operationerr_to_ldapresultcode(e);
|
|
return Ok(LdapResponseState::Respond(sr.gen_error(rc, msg)));
|
|
}
|
|
};
|
|
// If okay, do the search.
|
|
self.do_search(idms, &sr, &lbt, Source::Internal)
|
|
.await
|
|
.map(|r| LdapResponseState::BindMultiPartResponse(lbt, r))
|
|
.or_else(|e| {
|
|
let (rc, msg) = operationerr_to_ldapresultcode(e);
|
|
Ok(LdapResponseState::Respond(sr.gen_error(rc, msg)))
|
|
})
|
|
}
|
|
},
|
|
ServerOps::Unbind(_) => {
|
|
// No need to notify on unbind (per rfc4511)
|
|
Ok(LdapResponseState::Unbind)
|
|
}
|
|
ServerOps::Compare(cr) => match uat {
|
|
Some(u) => self
|
|
.do_compare(idms, &cr, &u, source)
|
|
.await
|
|
.map(LdapResponseState::MultiPartResponse)
|
|
.or_else(|e| {
|
|
let (rc, msg) = operationerr_to_ldapresultcode(e);
|
|
Ok(LdapResponseState::Respond(cr.gen_error(rc, msg)))
|
|
}),
|
|
None => {
|
|
// Compare can occur without a bind, so bind first.
|
|
// This is per section 4 of RFC 4513 (https://www.rfc-editor.org/rfc/rfc4513#section-4).
|
|
let lbt = match self.do_bind(idms, "", "").await {
|
|
Ok(Some(lbt)) => lbt,
|
|
Ok(None) => {
|
|
return Ok(LdapResponseState::Respond(
|
|
cr.gen_error(LdapResultCode::InvalidCredentials, "".to_string()),
|
|
))
|
|
}
|
|
Err(e) => {
|
|
let (rc, msg) = operationerr_to_ldapresultcode(e);
|
|
return Ok(LdapResponseState::Respond(cr.gen_error(rc, msg)));
|
|
}
|
|
};
|
|
// If okay, do the compare.
|
|
self.do_compare(idms, &cr, &lbt, Source::Internal)
|
|
.await
|
|
.map(|r| LdapResponseState::BindMultiPartResponse(lbt, r))
|
|
.or_else(|e| {
|
|
let (rc, msg) = operationerr_to_ldapresultcode(e);
|
|
Ok(LdapResponseState::Respond(cr.gen_error(rc, msg)))
|
|
})
|
|
}
|
|
},
|
|
ServerOps::Whoami(wr) => match uat {
|
|
Some(u) => Ok(LdapResponseState::Respond(
|
|
wr.gen_success(format!("u: {}", u.spn).as_str()),
|
|
)),
|
|
None => Ok(LdapResponseState::Respond(
|
|
wr.gen_operror(format!("Unbound Connection {eventid}").as_str()),
|
|
)),
|
|
},
|
|
} // end match server op
|
|
}
|
|
|
|
async fn bind_target_from_bind_dn(
|
|
&self,
|
|
idm_auth: &mut IdmServerAuthTransaction<'_>,
|
|
dn: &str,
|
|
pw: &str,
|
|
) -> Result<LdapBindTarget, OperationError> {
|
|
if dn.is_empty() {
|
|
if pw.is_empty() {
|
|
return Ok(LdapBindTarget::Account(UUID_ANONYMOUS));
|
|
} else {
|
|
// This is the path to access api-token logins.
|
|
return Ok(LdapBindTarget::ApiToken);
|
|
}
|
|
} else if dn == "dn=token" {
|
|
// Is the passed dn requesting token auth?
|
|
// We use dn= here since these are attr=value, and dn is a phantom so it will
|
|
// never be present or match a real value. We also make it an ava so that clients
|
|
// that over-zealously validate dn syntax are happy.
|
|
return Ok(LdapBindTarget::ApiToken);
|
|
}
|
|
|
|
if let Some(captures) = self.binddnre.captures(dn) {
|
|
if let Some(usr) = captures.name("val") {
|
|
let usr = usr.as_str();
|
|
|
|
if usr.is_empty() {
|
|
error!("Failed to parse user name from bind DN, it is empty (capture group is {:#?})", captures.name("val"));
|
|
return Err(OperationError::NoMatchingEntries);
|
|
}
|
|
|
|
let usr_uuid = idm_auth.qs_read.name_to_uuid(usr).map_err(|e| {
|
|
error!(err = ?e, ?usr, "Error resolving rdn to target");
|
|
e
|
|
})?;
|
|
|
|
if let Some(app) = captures.name("app") {
|
|
let app = app.as_str();
|
|
|
|
if app.is_empty() {
|
|
error!("Failed to parse application name from bind DN, it is empty (capture group is {:#?})", captures.name("app"));
|
|
return Err(OperationError::NoMatchingEntries);
|
|
}
|
|
|
|
return Ok(LdapBindTarget::Application(app.to_string(), usr_uuid));
|
|
}
|
|
|
|
return Ok(LdapBindTarget::Account(usr_uuid));
|
|
}
|
|
}
|
|
|
|
error!(
|
|
"Failed to parse bind DN, no captures. Bind DN was {:?})",
|
|
dn
|
|
);
|
|
Err(OperationError::NoMatchingEntries)
|
|
}
|
|
}
|
|
|
|
fn ldap_domain_to_dc(input: &str) -> String {
|
|
let mut output: String = String::new();
|
|
input.split('.').for_each(|dc| {
|
|
output.push_str("dc=");
|
|
output.push_str(dc);
|
|
#[allow(clippy::single_char_pattern, clippy::single_char_add_str)]
|
|
output.push_str(",");
|
|
});
|
|
// Remove the last ','
|
|
output.pop();
|
|
output
|
|
}
|
|
|
|
fn operationerr_to_ldapresultcode(e: OperationError) -> (LdapResultCode, String) {
|
|
match e {
|
|
OperationError::InvalidRequestState => {
|
|
(LdapResultCode::ConstraintViolation, "".to_string())
|
|
}
|
|
OperationError::InvalidAttributeName(s) | OperationError::InvalidAttribute(s) => {
|
|
(LdapResultCode::InvalidAttributeSyntax, s)
|
|
}
|
|
OperationError::SchemaViolation(se) => {
|
|
(LdapResultCode::UnwillingToPerform, format!("{se:?}"))
|
|
}
|
|
e => (LdapResultCode::Other, format!("{e:?}")),
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub(crate) fn ldap_all_vattrs() -> Vec<String> {
|
|
vec![
|
|
ATTR_CN.to_string(),
|
|
ATTR_EMAIL.to_string(),
|
|
ATTR_LDAP_EMAIL_ADDRESS.to_string(),
|
|
LDAP_ATTR_DN.to_string(),
|
|
LDAP_ATTR_EMAIL_ALTERNATIVE.to_string(),
|
|
LDAP_ATTR_EMAIL_PRIMARY.to_string(),
|
|
LDAP_ATTR_ENTRYDN.to_string(),
|
|
LDAP_ATTR_ENTRYUUID.to_string(),
|
|
LDAP_ATTR_KEYS.to_string(),
|
|
LDAP_ATTR_MAIL_ALTERNATIVE.to_string(),
|
|
LDAP_ATTR_MAIL_PRIMARY.to_string(),
|
|
ATTR_OBJECTCLASS.to_string(),
|
|
ATTR_LDAP_SSHPUBLICKEY.to_string(),
|
|
ATTR_UIDNUMBER.to_string(),
|
|
ATTR_UID.to_string(),
|
|
ATTR_GECOS.to_string(),
|
|
]
|
|
}
|
|
|
|
#[inline]
|
|
pub(crate) fn ldap_vattr_map(input: &str) -> Option<&str> {
|
|
// ⚠️ WARNING ⚠️
|
|
// If you modify this list you MUST add these values to
|
|
// corresponding phantom attributes in the schema to prevent
|
|
// incorrect future or duplicate usage.
|
|
//
|
|
// LDAP NAME KANI ATTR SOURCE NAME
|
|
match input {
|
|
// EntryDN and DN have special handling in to_ldap in Entry. However, we
|
|
// need to map them to "name" so that if the user has requested dn/entrydn
|
|
// only, then we still requested at least one attribute from the backend
|
|
// allowing the access control tests to take place. Otherwise no entries
|
|
// would be returned.
|
|
ATTR_CN | ATTR_UID | LDAP_ATTR_ENTRYDN | LDAP_ATTR_DN => Some(ATTR_NAME),
|
|
ATTR_GECOS => Some(ATTR_DISPLAYNAME),
|
|
ATTR_EMAIL => Some(ATTR_MAIL),
|
|
ATTR_LDAP_EMAIL_ADDRESS => Some(ATTR_MAIL),
|
|
LDAP_ATTR_EMAIL_ALTERNATIVE => Some(ATTR_MAIL),
|
|
LDAP_ATTR_EMAIL_PRIMARY => Some(ATTR_MAIL),
|
|
LDAP_ATTR_ENTRYUUID => Some(ATTR_UUID),
|
|
LDAP_ATTR_KEYS => Some(ATTR_SSH_PUBLICKEY),
|
|
LDAP_ATTR_MAIL_ALTERNATIVE => Some(ATTR_MAIL),
|
|
LDAP_ATTR_MAIL_PRIMARY => Some(ATTR_MAIL),
|
|
ATTR_OBJECTCLASS => Some(ATTR_CLASS),
|
|
ATTR_LDAP_SSHPUBLICKEY => Some(ATTR_SSH_PUBLICKEY), // no-underscore -> underscore
|
|
ATTR_UIDNUMBER => Some(ATTR_GIDNUMBER), // yes this is intentional
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub(crate) fn ldap_attr_filter_map(input: &str) -> Attribute {
|
|
let a_lower = input.to_lowercase();
|
|
Attribute::from(ldap_vattr_map(&a_lower).unwrap_or(a_lower.as_str()))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::prelude::*;
|
|
|
|
use compact_jwt::{dangernoverify::JwsDangerReleaseWithoutVerify, JwsVerifier};
|
|
use hashbrown::HashSet;
|
|
use kanidm_proto::internal::ApiToken;
|
|
use ldap3_proto::proto::{
|
|
LdapFilter, LdapMsg, LdapOp, LdapResultCode, LdapSearchScope, LdapSubstringFilter,
|
|
};
|
|
use ldap3_proto::simple::*;
|
|
|
|
use super::{LdapServer, LdapSession};
|
|
use crate::idm::application::GenerateApplicationPasswordEvent;
|
|
use crate::idm::event::{LdapApplicationAuthEvent, UnixPasswordChangeEvent};
|
|
use crate::idm::serviceaccount::GenerateApiTokenEvent;
|
|
|
|
const TEST_PASSWORD: &str = "ntaoeuntnaoeuhraohuercahu😍";
|
|
|
|
#[idm_test]
|
|
async fn test_ldap_simple_bind(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
|
|
let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
|
|
|
|
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
|
|
// make the admin a valid posix account
|
|
let me_posix = ModifyEvent::new_internal_invalid(
|
|
filter!(f_eq(Attribute::Name, PartialValue::new_iname("admin"))),
|
|
ModifyList::new_list(vec![
|
|
Modify::Present(Attribute::Class, EntryClass::PosixAccount.into()),
|
|
Modify::Present(Attribute::GidNumber, Value::new_uint32(2001)),
|
|
]),
|
|
);
|
|
assert!(idms_prox_write.qs_write.modify(&me_posix).is_ok());
|
|
|
|
let pce = UnixPasswordChangeEvent::new_internal(UUID_ADMIN, TEST_PASSWORD);
|
|
|
|
assert!(idms_prox_write.set_unix_account_password(&pce).is_ok());
|
|
assert!(idms_prox_write.commit().is_ok()); // Committing all configs
|
|
|
|
// default UNIX_PW bind (default is set to true)
|
|
// Hence allows all unix binds
|
|
let admin_t = ldaps
|
|
.do_bind(idms, "admin", TEST_PASSWORD)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));
|
|
let admin_t = ldaps
|
|
.do_bind(idms, "admin@example.com", TEST_PASSWORD)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));
|
|
|
|
// Setting UNIX_PW_BIND flag to false:
|
|
// Hence all of the below authentication will fail (asserts are still satisfied)
|
|
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
|
|
let disallow_unix_pw_flag = ModifyEvent::new_internal_invalid(
|
|
filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_DOMAIN_INFO))),
|
|
ModifyList::new_purge_and_set(Attribute::LdapAllowUnixPwBind, Value::Bool(false)),
|
|
);
|
|
assert!(idms_prox_write
|
|
.qs_write
|
|
.modify(&disallow_unix_pw_flag)
|
|
.is_ok());
|
|
assert!(idms_prox_write.commit().is_ok());
|
|
let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
|
|
assert_eq!(
|
|
anon_t.effective_session,
|
|
LdapSession::UnixBind(UUID_ANONYMOUS)
|
|
);
|
|
assert!(
|
|
ldaps.do_bind(idms, "", "test").await.unwrap_err() == OperationError::NotAuthenticated
|
|
);
|
|
let admin_t = ldaps.do_bind(idms, "admin", TEST_PASSWORD).await.unwrap();
|
|
assert!(admin_t.is_none());
|
|
|
|
// Setting UNIX_PW_BIND flag to true :
|
|
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
|
|
let allow_unix_pw_flag = ModifyEvent::new_internal_invalid(
|
|
filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_DOMAIN_INFO))),
|
|
ModifyList::new_purge_and_set(Attribute::LdapAllowUnixPwBind, Value::Bool(true)),
|
|
);
|
|
assert!(idms_prox_write.qs_write.modify(&allow_unix_pw_flag).is_ok());
|
|
assert!(idms_prox_write.commit().is_ok());
|
|
|
|
// Now test the admin and various DN's
|
|
let admin_t = ldaps
|
|
.do_bind(idms, "admin", TEST_PASSWORD)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));
|
|
let admin_t = ldaps
|
|
.do_bind(idms, "admin@example.com", TEST_PASSWORD)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));
|
|
let admin_t = ldaps
|
|
.do_bind(idms, STR_UUID_ADMIN, TEST_PASSWORD)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));
|
|
let admin_t = ldaps
|
|
.do_bind(idms, "name=admin,dc=example,dc=com", TEST_PASSWORD)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));
|
|
let admin_t = ldaps
|
|
.do_bind(
|
|
idms,
|
|
"spn=admin@example.com,dc=example,dc=com",
|
|
TEST_PASSWORD,
|
|
)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));
|
|
let admin_t = ldaps
|
|
.do_bind(
|
|
idms,
|
|
format!("uuid={STR_UUID_ADMIN},dc=example,dc=com").as_str(),
|
|
TEST_PASSWORD,
|
|
)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));
|
|
|
|
let admin_t = ldaps
|
|
.do_bind(idms, "name=admin", TEST_PASSWORD)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));
|
|
let admin_t = ldaps
|
|
.do_bind(idms, "spn=admin@example.com", TEST_PASSWORD)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));
|
|
let admin_t = ldaps
|
|
.do_bind(
|
|
idms,
|
|
format!("uuid={STR_UUID_ADMIN}").as_str(),
|
|
TEST_PASSWORD,
|
|
)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));
|
|
|
|
let admin_t = ldaps
|
|
.do_bind(idms, "admin,dc=example,dc=com", TEST_PASSWORD)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));
|
|
let admin_t = ldaps
|
|
.do_bind(idms, "admin@example.com,dc=example,dc=com", TEST_PASSWORD)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));
|
|
let admin_t = ldaps
|
|
.do_bind(
|
|
idms,
|
|
format!("{STR_UUID_ADMIN},dc=example,dc=com").as_str(),
|
|
TEST_PASSWORD,
|
|
)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));
|
|
|
|
// Bad password, check last to prevent softlocking of the admin account.
|
|
assert!(ldaps
|
|
.do_bind(idms, "admin", "test")
|
|
.await
|
|
.unwrap()
|
|
.is_none());
|
|
|
|
// Non-existent and invalid DNs
|
|
assert!(ldaps
|
|
.do_bind(
|
|
idms,
|
|
"spn=admin@example.com,dc=clownshoes,dc=example,dc=com",
|
|
TEST_PASSWORD
|
|
)
|
|
.await
|
|
.is_err());
|
|
assert!(ldaps
|
|
.do_bind(
|
|
idms,
|
|
"spn=claire@example.com,dc=example,dc=com",
|
|
TEST_PASSWORD
|
|
)
|
|
.await
|
|
.is_err());
|
|
assert!(ldaps
|
|
.do_bind(idms, ",dc=example,dc=com", TEST_PASSWORD)
|
|
.await
|
|
.is_err());
|
|
assert!(ldaps
|
|
.do_bind(idms, "dc=example,dc=com", TEST_PASSWORD)
|
|
.await
|
|
.is_err());
|
|
|
|
assert!(ldaps.do_bind(idms, "claire", "test").await.is_err());
|
|
}
|
|
|
|
#[idm_test]
|
|
async fn test_ldap_application_dnre(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
|
|
let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
|
|
|
|
let testdn = format!("app=app1,{0}", ldaps.basedn);
|
|
let captures = ldaps.dnre.captures(testdn.as_str()).unwrap();
|
|
assert!(captures.name("app").is_some());
|
|
assert!(captures.name("attr").is_none());
|
|
assert!(captures.name("val").is_none());
|
|
|
|
let testdn = format!("uid=foo,app=app1,{0}", ldaps.basedn);
|
|
let captures = ldaps.dnre.captures(testdn.as_str()).unwrap();
|
|
assert!(captures.name("app").is_some());
|
|
assert!(captures.name("attr").is_some());
|
|
assert!(captures.name("val").is_some());
|
|
|
|
let testdn = format!("uid=foo,{0}", ldaps.basedn);
|
|
let captures = ldaps.dnre.captures(testdn.as_str()).unwrap();
|
|
assert!(captures.name("app").is_none());
|
|
assert!(captures.name("attr").is_some());
|
|
assert!(captures.name("val").is_some());
|
|
}
|
|
|
|
#[idm_test]
|
|
async fn test_ldap_application_search(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
|
|
let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
|
|
|
|
let usr_uuid = Uuid::new_v4();
|
|
let grp_uuid = Uuid::new_v4();
|
|
let app_uuid = Uuid::new_v4();
|
|
let app_name = "testapp1";
|
|
|
|
// Setup person, group and application
|
|
{
|
|
let e1 = entry_init!(
|
|
(Attribute::Class, EntryClass::Object.to_value()),
|
|
(Attribute::Class, EntryClass::Account.to_value()),
|
|
(Attribute::Class, EntryClass::Person.to_value()),
|
|
(Attribute::Name, Value::new_iname("testperson1")),
|
|
(Attribute::Uuid, Value::Uuid(usr_uuid)),
|
|
(Attribute::Description, Value::new_utf8s("testperson1")),
|
|
(Attribute::DisplayName, Value::new_utf8s("testperson1"))
|
|
);
|
|
|
|
let e2 = entry_init!(
|
|
(Attribute::Class, EntryClass::Object.to_value()),
|
|
(Attribute::Class, EntryClass::Group.to_value()),
|
|
(Attribute::Name, Value::new_iname("testgroup1")),
|
|
(Attribute::Uuid, Value::Uuid(grp_uuid))
|
|
);
|
|
|
|
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))
|
|
);
|
|
|
|
let ct = duration_from_epoch_now();
|
|
let mut server_txn = idms.proxy_write(ct).await.unwrap();
|
|
assert!(server_txn
|
|
.qs_write
|
|
.internal_create(vec![e1, e2, e3])
|
|
.and_then(|_| server_txn.commit())
|
|
.is_ok());
|
|
}
|
|
|
|
// Setup the anonymous login
|
|
let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
|
|
assert_eq!(
|
|
anon_t.effective_session,
|
|
LdapSession::UnixBind(UUID_ANONYMOUS)
|
|
);
|
|
|
|
// Searches under application base DN must show same content
|
|
let sr = SearchRequest {
|
|
msgid: 1,
|
|
base: format!("app={app_name},dc=example,dc=com"),
|
|
scope: LdapSearchScope::Subtree,
|
|
filter: LdapFilter::Present(Attribute::ObjectClass.to_string()),
|
|
attrs: vec!["*".to_string()],
|
|
};
|
|
|
|
let r1 = ldaps
|
|
.do_search(idms, &sr, &anon_t, Source::Internal)
|
|
.await
|
|
.unwrap();
|
|
|
|
let sr = SearchRequest {
|
|
msgid: 1,
|
|
base: "dc=example,dc=com".to_string(),
|
|
scope: LdapSearchScope::Subtree,
|
|
filter: LdapFilter::Present(Attribute::ObjectClass.to_string()),
|
|
attrs: vec!["*".to_string()],
|
|
};
|
|
|
|
let r2 = ldaps
|
|
.do_search(idms, &sr, &anon_t, Source::Internal)
|
|
.await
|
|
.unwrap();
|
|
assert!(!r1.is_empty());
|
|
assert_eq!(r1.len(), r2.len());
|
|
}
|
|
|
|
#[idm_test]
|
|
async fn test_ldap_spn_search(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
|
|
let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
|
|
|
|
let usr_uuid = Uuid::new_v4();
|
|
let usr_name = "panko";
|
|
|
|
// Setup person, group and application
|
|
{
|
|
let e1: Entry<EntryInit, EntryNew> = entry_init!(
|
|
(Attribute::Class, EntryClass::Object.to_value()),
|
|
(Attribute::Class, EntryClass::Account.to_value()),
|
|
(Attribute::Class, EntryClass::Person.to_value()),
|
|
(Attribute::Name, Value::new_iname(usr_name)),
|
|
(Attribute::Uuid, Value::Uuid(usr_uuid)),
|
|
(Attribute::DisplayName, Value::new_utf8s(usr_name))
|
|
);
|
|
|
|
let ct = duration_from_epoch_now();
|
|
let mut server_txn = idms.proxy_write(ct).await.unwrap();
|
|
assert!(server_txn
|
|
.qs_write
|
|
.internal_create(vec![e1])
|
|
.and_then(|_| server_txn.commit())
|
|
.is_ok());
|
|
}
|
|
|
|
// Setup the anonymous login
|
|
let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
|
|
assert_eq!(
|
|
anon_t.effective_session,
|
|
LdapSession::UnixBind(UUID_ANONYMOUS)
|
|
);
|
|
|
|
// Searching a malformed spn shouldn't cause the query to fail
|
|
let sr = SearchRequest {
|
|
msgid: 1,
|
|
base: format!("dc=example,dc=com"),
|
|
scope: LdapSearchScope::Subtree,
|
|
filter: LdapFilter::Or(vec![
|
|
LdapFilter::Equality(Attribute::Name.to_string(), usr_name.to_string()),
|
|
LdapFilter::Equality(Attribute::Spn.to_string(), usr_name.to_string()),
|
|
]),
|
|
attrs: vec!["*".to_string()],
|
|
};
|
|
|
|
let result = ldaps
|
|
.do_search(idms, &sr, &anon_t, Source::Internal)
|
|
.await
|
|
.map(|r| {
|
|
r.into_iter()
|
|
.filter(|r| matches!(r.op, LdapOp::SearchResultEntry(_)))
|
|
.collect::<Vec<_>>()
|
|
})
|
|
.unwrap();
|
|
|
|
assert!(!result.is_empty());
|
|
|
|
let sr = SearchRequest {
|
|
msgid: 1,
|
|
base: format!("dc=example,dc=com"),
|
|
scope: LdapSearchScope::Subtree,
|
|
filter: LdapFilter::And(vec![
|
|
LdapFilter::Equality(Attribute::Name.to_string(), usr_name.to_string()),
|
|
LdapFilter::Equality(Attribute::Spn.to_string(), usr_name.to_string()),
|
|
]),
|
|
attrs: vec!["*".to_string()],
|
|
};
|
|
|
|
let empty_result = ldaps
|
|
.do_search(idms, &sr, &anon_t, Source::Internal)
|
|
.await
|
|
.map(|r| {
|
|
r.into_iter()
|
|
.filter(|r| matches!(r.op, LdapOp::SearchResultEntry(_)))
|
|
.collect::<Vec<_>>()
|
|
})
|
|
.unwrap();
|
|
|
|
assert!(empty_result.is_empty());
|
|
}
|
|
|
|
#[idm_test]
|
|
async fn test_ldap_application_bind(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
|
|
let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
|
|
|
|
let usr_uuid = Uuid::new_v4();
|
|
let grp_uuid = Uuid::new_v4();
|
|
let app_uuid = Uuid::new_v4();
|
|
|
|
// Setup person, group and application
|
|
{
|
|
let e1 = entry_init!(
|
|
(Attribute::Class, EntryClass::Object.to_value()),
|
|
(Attribute::Class, EntryClass::Account.to_value()),
|
|
(Attribute::Class, EntryClass::Person.to_value()),
|
|
(Attribute::Name, Value::new_iname("testperson1")),
|
|
(Attribute::Uuid, Value::Uuid(usr_uuid)),
|
|
(Attribute::Description, Value::new_utf8s("testperson1")),
|
|
(Attribute::DisplayName, Value::new_utf8s("testperson1"))
|
|
);
|
|
|
|
let e2 = entry_init!(
|
|
(Attribute::Class, EntryClass::Object.to_value()),
|
|
(Attribute::Class, EntryClass::Group.to_value()),
|
|
(Attribute::Name, Value::new_iname("testgroup1")),
|
|
(Attribute::Uuid, Value::Uuid(grp_uuid))
|
|
);
|
|
|
|
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))
|
|
);
|
|
|
|
let ct = duration_from_epoch_now();
|
|
let mut server_txn = idms.proxy_write(ct).await.unwrap();
|
|
assert!(server_txn
|
|
.qs_write
|
|
.internal_create(vec![e1, e2, e3])
|
|
.and_then(|_| server_txn.commit())
|
|
.is_ok());
|
|
}
|
|
|
|
// No session, user not member of linked group
|
|
let res = ldaps
|
|
.do_bind(idms, "spn=testperson1,app=testapp1,dc=example,dc=com", "")
|
|
.await;
|
|
assert!(res.is_ok());
|
|
assert!(res.unwrap().is_none());
|
|
|
|
{
|
|
let ml = ModifyList::new_append(Attribute::Member, Value::Refer(usr_uuid));
|
|
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
|
|
assert!(idms_prox_write
|
|
.qs_write
|
|
.internal_modify_uuid(grp_uuid, &ml)
|
|
.is_ok());
|
|
assert!(idms_prox_write.commit().is_ok());
|
|
}
|
|
|
|
// No session, user does not have app password for testapp1
|
|
let res = ldaps
|
|
.do_bind(idms, "spn=testperson1,app=testapp1,dc=example,dc=com", "")
|
|
.await;
|
|
assert!(res.is_ok());
|
|
assert!(res.unwrap().is_none());
|
|
|
|
let pass1: String;
|
|
let pass2: String;
|
|
let pass3: String;
|
|
{
|
|
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
|
|
|
|
let ev = GenerateApplicationPasswordEvent::new_internal(
|
|
usr_uuid,
|
|
app_uuid,
|
|
"apppwd1".to_string(),
|
|
);
|
|
pass1 = idms_prox_write
|
|
.generate_application_password(&ev)
|
|
.expect("Failed to generate application password");
|
|
|
|
let ev = GenerateApplicationPasswordEvent::new_internal(
|
|
usr_uuid,
|
|
app_uuid,
|
|
"apppwd2".to_string(),
|
|
);
|
|
pass2 = idms_prox_write
|
|
.generate_application_password(&ev)
|
|
.expect("Failed to generate application password");
|
|
|
|
assert!(idms_prox_write.commit().is_ok());
|
|
|
|
// Application password overwritten on duplicated label
|
|
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
|
|
let ev = GenerateApplicationPasswordEvent::new_internal(
|
|
usr_uuid,
|
|
app_uuid,
|
|
"apppwd2".to_string(),
|
|
);
|
|
pass3 = idms_prox_write
|
|
.generate_application_password(&ev)
|
|
.expect("Failed to generate application password");
|
|
assert!(idms_prox_write.commit().is_ok());
|
|
}
|
|
|
|
// Got session, app password valid
|
|
let res = ldaps
|
|
.do_bind(
|
|
idms,
|
|
"spn=testperson1,app=testapp1,dc=example,dc=com",
|
|
pass1.as_str(),
|
|
)
|
|
.await;
|
|
assert!(res.is_ok());
|
|
assert!(res.unwrap().is_some());
|
|
|
|
// No session, app password overwritten
|
|
let res = ldaps
|
|
.do_bind(
|
|
idms,
|
|
"spn=testperson1,app=testapp1,dc=example,dc=com",
|
|
pass2.as_str(),
|
|
)
|
|
.await;
|
|
assert!(res.is_ok());
|
|
assert!(res.unwrap().is_none());
|
|
|
|
// Got session, app password overwritten
|
|
let res = ldaps
|
|
.do_bind(
|
|
idms,
|
|
"spn=testperson1,app=testapp1,dc=example,dc=com",
|
|
pass3.as_str(),
|
|
)
|
|
.await;
|
|
assert!(res.is_ok());
|
|
assert!(res.unwrap().is_some());
|
|
|
|
// No session, invalid app password
|
|
let res = ldaps
|
|
.do_bind(
|
|
idms,
|
|
"spn=testperson1,app=testapp1,dc=example,dc=com",
|
|
"FOO",
|
|
)
|
|
.await;
|
|
assert!(res.is_ok());
|
|
assert!(res.unwrap().is_none());
|
|
}
|
|
|
|
#[idm_test]
|
|
async fn test_ldap_application_linked_group(
|
|
idms: &IdmServer,
|
|
_idms_delayed: &IdmServerDelayed,
|
|
) {
|
|
let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
|
|
|
|
let usr_uuid = Uuid::new_v4();
|
|
let usr_name = "testuser1";
|
|
|
|
let grp1_uuid = Uuid::new_v4();
|
|
let grp1_name = "testgroup1";
|
|
let grp2_uuid = Uuid::new_v4();
|
|
let grp2_name = "testgroup2";
|
|
|
|
let app1_uuid = Uuid::new_v4();
|
|
let app1_name = "testapp1";
|
|
let app2_uuid = Uuid::new_v4();
|
|
let app2_name = "testapp2";
|
|
|
|
// Setup person, groups and applications
|
|
{
|
|
let e1 = entry_init!(
|
|
(Attribute::Class, EntryClass::Object.to_value()),
|
|
(Attribute::Class, EntryClass::Account.to_value()),
|
|
(Attribute::Class, EntryClass::Person.to_value()),
|
|
(Attribute::Name, Value::new_iname(usr_name)),
|
|
(Attribute::Uuid, Value::Uuid(usr_uuid)),
|
|
(Attribute::Description, Value::new_utf8s(usr_name)),
|
|
(Attribute::DisplayName, Value::new_utf8s(usr_name))
|
|
);
|
|
|
|
let e2 = entry_init!(
|
|
(Attribute::Class, EntryClass::Object.to_value()),
|
|
(Attribute::Class, EntryClass::Group.to_value()),
|
|
(Attribute::Name, Value::new_iname(grp1_name)),
|
|
(Attribute::Uuid, Value::Uuid(grp1_uuid)),
|
|
(Attribute::Member, Value::Refer(usr_uuid))
|
|
);
|
|
|
|
let e3 = entry_init!(
|
|
(Attribute::Class, EntryClass::Object.to_value()),
|
|
(Attribute::Class, EntryClass::Group.to_value()),
|
|
(Attribute::Name, Value::new_iname(grp2_name)),
|
|
(Attribute::Uuid, Value::Uuid(grp2_uuid))
|
|
);
|
|
|
|
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))
|
|
);
|
|
|
|
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))
|
|
);
|
|
|
|
let ct = duration_from_epoch_now();
|
|
let mut server_txn = idms.proxy_write(ct).await.unwrap();
|
|
assert!(server_txn
|
|
.qs_write
|
|
.internal_create(vec![e1, e2, e3, e4, e5])
|
|
.and_then(|_| server_txn.commit())
|
|
.is_ok());
|
|
}
|
|
|
|
let pass_app1: String;
|
|
let pass_app2: String;
|
|
{
|
|
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
|
|
|
|
let ev = GenerateApplicationPasswordEvent::new_internal(
|
|
usr_uuid,
|
|
app1_uuid,
|
|
"label".to_string(),
|
|
);
|
|
pass_app1 = idms_prox_write
|
|
.generate_application_password(&ev)
|
|
.expect("Failed to generate application password");
|
|
|
|
// It is possible to generate an application password even if the
|
|
// user is not member of the linked group
|
|
let ev = GenerateApplicationPasswordEvent::new_internal(
|
|
usr_uuid,
|
|
app2_uuid,
|
|
"label".to_string(),
|
|
);
|
|
pass_app2 = idms_prox_write
|
|
.generate_application_password(&ev)
|
|
.expect("Failed to generate application password");
|
|
|
|
assert!(idms_prox_write.commit().is_ok());
|
|
}
|
|
|
|
// Got session, app password valid
|
|
let res = ldaps
|
|
.do_bind(
|
|
idms,
|
|
format!("spn={usr_name},app={app1_name},dc=example,dc=com").as_str(),
|
|
pass_app1.as_str(),
|
|
)
|
|
.await;
|
|
assert!(res.is_ok());
|
|
assert!(res.unwrap().is_some());
|
|
|
|
// No session, not member
|
|
let res = ldaps
|
|
.do_bind(
|
|
idms,
|
|
format!("spn={usr_name},app={app2_name},dc=example,dc=com").as_str(),
|
|
pass_app2.as_str(),
|
|
)
|
|
.await;
|
|
assert!(res.is_ok());
|
|
assert!(res.unwrap().is_none());
|
|
|
|
// Add user to grp2
|
|
{
|
|
let ml = ModifyList::new_append(Attribute::Member, Value::Refer(usr_uuid));
|
|
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
|
|
assert!(idms_prox_write
|
|
.qs_write
|
|
.internal_modify_uuid(grp2_uuid, &ml)
|
|
.is_ok());
|
|
assert!(idms_prox_write.commit().is_ok());
|
|
}
|
|
|
|
// Got session, app password valid
|
|
let res = ldaps
|
|
.do_bind(
|
|
idms,
|
|
format!("spn={usr_name},app={app2_name},dc=example,dc=com").as_str(),
|
|
pass_app2.as_str(),
|
|
)
|
|
.await;
|
|
assert!(res.is_ok());
|
|
assert!(res.unwrap().is_some());
|
|
|
|
// No session, wrong app
|
|
let res = ldaps
|
|
.do_bind(
|
|
idms,
|
|
format!("spn={usr_name},app={app1_name},dc=example,dc=com").as_str(),
|
|
pass_app2.as_str(),
|
|
)
|
|
.await;
|
|
assert!(res.is_ok());
|
|
assert!(res.unwrap().is_none());
|
|
|
|
// Bind error, app not exists
|
|
{
|
|
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
|
|
let de = DeleteEvent::new_internal_invalid(filter!(f_eq(
|
|
Attribute::Uuid,
|
|
PartialValue::Uuid(app2_uuid)
|
|
)));
|
|
assert!(idms_prox_write.qs_write.delete(&de).is_ok());
|
|
assert!(idms_prox_write.commit().is_ok());
|
|
}
|
|
|
|
let res = ldaps
|
|
.do_bind(
|
|
idms,
|
|
format!("spn={usr_name},app={app2_name},dc=example,dc=com").as_str(),
|
|
pass_app2.as_str(),
|
|
)
|
|
.await;
|
|
assert!(res.is_err());
|
|
}
|
|
|
|
// For testing the timeouts
|
|
// We need times on this scale
|
|
// not yet valid <-> valid from time <-> current_time <-> expire time <-> expired
|
|
const TEST_CURRENT_TIME: u64 = 6000;
|
|
const TEST_NOT_YET_VALID_TIME: u64 = TEST_CURRENT_TIME - 240;
|
|
const TEST_VALID_FROM_TIME: u64 = TEST_CURRENT_TIME - 120;
|
|
const TEST_EXPIRE_TIME: u64 = TEST_CURRENT_TIME + 120;
|
|
const TEST_AFTER_EXPIRY: u64 = TEST_CURRENT_TIME + 240;
|
|
|
|
async fn set_account_valid_time(idms: &IdmServer, acct: Uuid) {
|
|
let mut idms_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
|
|
|
|
let v_valid_from = Value::new_datetime_epoch(Duration::from_secs(TEST_VALID_FROM_TIME));
|
|
let v_expire = Value::new_datetime_epoch(Duration::from_secs(TEST_EXPIRE_TIME));
|
|
|
|
let me = ModifyEvent::new_internal_invalid(
|
|
filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(acct))),
|
|
ModifyList::new_list(vec![
|
|
Modify::Present(Attribute::AccountExpire, v_expire),
|
|
Modify::Present(Attribute::AccountValidFrom, v_valid_from),
|
|
]),
|
|
);
|
|
assert!(idms_write.qs_write.modify(&me).is_ok());
|
|
idms_write.commit().expect("Must not fail");
|
|
}
|
|
|
|
#[idm_test]
|
|
async fn test_ldap_application_valid_from_expire(
|
|
idms: &IdmServer,
|
|
_idms_delayed: &IdmServerDelayed,
|
|
) {
|
|
let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
|
|
|
|
let usr_uuid = Uuid::new_v4();
|
|
let usr_name = "testuser1";
|
|
|
|
let grp1_uuid = Uuid::new_v4();
|
|
let grp1_name = "testgroup1";
|
|
|
|
let app1_uuid = Uuid::new_v4();
|
|
let app1_name = "testapp1";
|
|
|
|
let pass_app1: String;
|
|
|
|
// Setup person, group, application and app password
|
|
{
|
|
let e1 = entry_init!(
|
|
(Attribute::Class, EntryClass::Object.to_value()),
|
|
(Attribute::Class, EntryClass::Account.to_value()),
|
|
(Attribute::Class, EntryClass::Person.to_value()),
|
|
(Attribute::Name, Value::new_iname(usr_name)),
|
|
(Attribute::Uuid, Value::Uuid(usr_uuid)),
|
|
(Attribute::Description, Value::new_utf8s(usr_name)),
|
|
(Attribute::DisplayName, Value::new_utf8s(usr_name))
|
|
);
|
|
|
|
let e2 = entry_init!(
|
|
(Attribute::Class, EntryClass::Object.to_value()),
|
|
(Attribute::Class, EntryClass::Group.to_value()),
|
|
(Attribute::Name, Value::new_iname(grp1_name)),
|
|
(Attribute::Uuid, Value::Uuid(grp1_uuid)),
|
|
(Attribute::Member, Value::Refer(usr_uuid))
|
|
);
|
|
|
|
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))
|
|
);
|
|
|
|
let ct = duration_from_epoch_now();
|
|
let mut server_txn = idms.proxy_write(ct).await.unwrap();
|
|
assert!(server_txn
|
|
.qs_write
|
|
.internal_create(vec![e1, e2, e3])
|
|
.and_then(|_| server_txn.commit())
|
|
.is_ok());
|
|
|
|
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
|
|
|
|
let ev = GenerateApplicationPasswordEvent::new_internal(
|
|
usr_uuid,
|
|
app1_uuid,
|
|
"label".to_string(),
|
|
);
|
|
pass_app1 = idms_prox_write
|
|
.generate_application_password(&ev)
|
|
.expect("Failed to generate application password");
|
|
|
|
assert!(idms_prox_write.commit().is_ok());
|
|
}
|
|
|
|
// Got session, app password valid
|
|
let res = ldaps
|
|
.do_bind(
|
|
idms,
|
|
format!("spn={usr_name},app={app1_name},dc=example,dc=com").as_str(),
|
|
pass_app1.as_str(),
|
|
)
|
|
.await;
|
|
assert!(res.is_ok());
|
|
assert!(res.unwrap().is_some());
|
|
|
|
// Any account that is not yet valid / expired can't auth.
|
|
// Set the valid bounds high/low
|
|
// TEST_VALID_FROM_TIME/TEST_EXPIRE_TIME
|
|
set_account_valid_time(idms, usr_uuid).await;
|
|
|
|
let time_low = Duration::from_secs(TEST_NOT_YET_VALID_TIME);
|
|
let time = Duration::from_secs(TEST_CURRENT_TIME);
|
|
let time_high = Duration::from_secs(TEST_AFTER_EXPIRY);
|
|
|
|
let mut idms_auth = idms.auth().await.unwrap();
|
|
let lae = LdapApplicationAuthEvent::new(app1_name, usr_uuid, pass_app1)
|
|
.expect("Failed to build auth event");
|
|
|
|
let r1 = idms_auth
|
|
.application_auth_ldap(&lae, time_low)
|
|
.await
|
|
.expect_err("Authentication succeeded");
|
|
assert_eq!(r1, OperationError::SessionExpired);
|
|
|
|
let r1 = idms_auth
|
|
.application_auth_ldap(&lae, time)
|
|
.await
|
|
.expect("Failed auth");
|
|
assert!(r1.is_some());
|
|
|
|
let r1 = idms_auth
|
|
.application_auth_ldap(&lae, time_high)
|
|
.await
|
|
.expect_err("Authentication succeeded");
|
|
assert_eq!(r1, OperationError::SessionExpired);
|
|
}
|
|
|
|
macro_rules! assert_entry_contains {
|
|
(
|
|
$entry:expr,
|
|
$dn:expr,
|
|
$($item:expr),*
|
|
) => {{
|
|
assert_eq!($entry.dn, $dn);
|
|
// Build a set from the attrs.
|
|
let mut attrs = HashSet::new();
|
|
for a in $entry.attributes.iter() {
|
|
for v in a.vals.iter() {
|
|
attrs.insert((a.atype.as_str(), v.as_slice()));
|
|
}
|
|
};
|
|
info!(?attrs);
|
|
$(
|
|
warn!("{}", $item.0);
|
|
assert!(attrs.contains(&(
|
|
$item.0.as_ref(), $item.1.as_bytes()
|
|
)));
|
|
)*
|
|
|
|
}};
|
|
}
|
|
|
|
#[idm_test]
|
|
async fn test_ldap_virtual_attribute_generation(
|
|
idms: &IdmServer,
|
|
_idms_delayed: &IdmServerDelayed,
|
|
) {
|
|
let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
|
|
|
|
let ssh_ed25519 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAeGW1P6Pc2rPq0XqbRaDKBcXZUPRklo0L1EyR30CwoP william@amethyst";
|
|
|
|
// Setup a user we want to check.
|
|
{
|
|
let e1 = entry_init!(
|
|
(Attribute::Class, EntryClass::Object.to_value()),
|
|
(Attribute::Class, EntryClass::Person.to_value()),
|
|
(Attribute::Class, EntryClass::Account.to_value()),
|
|
(Attribute::Class, EntryClass::PosixAccount.to_value()),
|
|
(Attribute::Name, Value::new_iname("testperson1")),
|
|
(
|
|
Attribute::Uuid,
|
|
Value::Uuid(uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"))
|
|
),
|
|
(Attribute::Description, Value::new_utf8s("testperson1")),
|
|
(Attribute::DisplayName, Value::new_utf8s("testperson1")),
|
|
(Attribute::GidNumber, Value::new_uint32(12345)),
|
|
(Attribute::LoginShell, Value::new_iutf8("/bin/zsh")),
|
|
(
|
|
Attribute::SshPublicKey,
|
|
Value::new_sshkey_str("test", ssh_ed25519).expect("Invalid ssh key")
|
|
)
|
|
);
|
|
|
|
let mut server_txn = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
|
|
let ce = CreateEvent::new_internal(vec![e1]);
|
|
assert!(server_txn
|
|
.qs_write
|
|
.create(&ce)
|
|
.and_then(|_| server_txn.commit())
|
|
.is_ok());
|
|
}
|
|
|
|
// Setup the anonymous login.
|
|
let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
|
|
assert_eq!(
|
|
anon_t.effective_session,
|
|
LdapSession::UnixBind(UUID_ANONYMOUS)
|
|
);
|
|
|
|
// Check that when we request *, we get default list.
|
|
let sr = SearchRequest {
|
|
msgid: 1,
|
|
base: "dc=example,dc=com".to_string(),
|
|
scope: LdapSearchScope::Subtree,
|
|
filter: LdapFilter::Equality(Attribute::Name.to_string(), "testperson1".to_string()),
|
|
attrs: vec!["*".to_string()],
|
|
};
|
|
let r1 = ldaps
|
|
.do_search(idms, &sr, &anon_t, Source::Internal)
|
|
.await
|
|
.unwrap();
|
|
|
|
// The result, and the ldap proto success msg.
|
|
assert_eq!(r1.len(), 2);
|
|
match &r1[0].op {
|
|
LdapOp::SearchResultEntry(lsre) => {
|
|
assert_entry_contains!(
|
|
lsre,
|
|
"spn=testperson1@example.com,dc=example,dc=com",
|
|
(Attribute::Class, EntryClass::Object.to_string()),
|
|
(Attribute::Class, EntryClass::Person.to_string()),
|
|
(Attribute::Class, EntryClass::Account.to_string()),
|
|
(Attribute::Class, EntryClass::PosixAccount.to_string()),
|
|
(Attribute::DisplayName, "testperson1"),
|
|
(Attribute::Name, "testperson1"),
|
|
(Attribute::GidNumber, "12345"),
|
|
(Attribute::LoginShell, "/bin/zsh"),
|
|
(Attribute::SshPublicKey, ssh_ed25519),
|
|
(Attribute::Uuid, "cc8e95b4-c24f-4d68-ba54-8bed76f63930")
|
|
);
|
|
}
|
|
_ => panic!("Oh no"),
|
|
};
|
|
|
|
// Check that when we request +, we get all attrs and the vattrs
|
|
let sr = SearchRequest {
|
|
msgid: 1,
|
|
base: "dc=example,dc=com".to_string(),
|
|
scope: LdapSearchScope::Subtree,
|
|
filter: LdapFilter::Equality(Attribute::Name.to_string(), "testperson1".to_string()),
|
|
attrs: vec!["+".to_string()],
|
|
};
|
|
let r1 = ldaps
|
|
.do_search(idms, &sr, &anon_t, Source::Internal)
|
|
.await
|
|
.unwrap();
|
|
|
|
// The result, and the ldap proto success msg.
|
|
assert_eq!(r1.len(), 2);
|
|
match &r1[0].op {
|
|
LdapOp::SearchResultEntry(lsre) => {
|
|
assert_entry_contains!(
|
|
lsre,
|
|
"spn=testperson1@example.com,dc=example,dc=com",
|
|
(Attribute::ObjectClass, EntryClass::Object.as_ref()),
|
|
(Attribute::ObjectClass, EntryClass::Person.as_ref()),
|
|
(Attribute::ObjectClass, EntryClass::Account.as_ref()),
|
|
(Attribute::ObjectClass, EntryClass::PosixAccount.as_ref()),
|
|
(Attribute::DisplayName, "testperson1"),
|
|
(Attribute::Name, "testperson1"),
|
|
(Attribute::GidNumber, "12345"),
|
|
(Attribute::LoginShell, "/bin/zsh"),
|
|
(Attribute::SshPublicKey, ssh_ed25519),
|
|
(Attribute::EntryUuid, "cc8e95b4-c24f-4d68-ba54-8bed76f63930"),
|
|
(
|
|
Attribute::EntryDn,
|
|
"spn=testperson1@example.com,dc=example,dc=com"
|
|
),
|
|
(Attribute::UidNumber, "12345"),
|
|
(Attribute::Cn, "testperson1"),
|
|
(Attribute::LdapKeys, ssh_ed25519)
|
|
);
|
|
}
|
|
_ => panic!("Oh no"),
|
|
};
|
|
|
|
// Check that when we request an attr by name, we get all of them correctly.
|
|
let sr = SearchRequest {
|
|
msgid: 1,
|
|
base: "dc=example,dc=com".to_string(),
|
|
scope: LdapSearchScope::Subtree,
|
|
filter: LdapFilter::Equality(Attribute::Name.to_string(), "testperson1".to_string()),
|
|
attrs: vec![
|
|
LDAP_ATTR_NAME.to_string(),
|
|
Attribute::EntryDn.to_string(),
|
|
ATTR_LDAP_KEYS.to_string(),
|
|
Attribute::UidNumber.to_string(),
|
|
],
|
|
};
|
|
let r1 = ldaps
|
|
.do_search(idms, &sr, &anon_t, Source::Internal)
|
|
.await
|
|
.unwrap();
|
|
|
|
// The result, and the ldap proto success msg.
|
|
assert_eq!(r1.len(), 2);
|
|
match &r1[0].op {
|
|
LdapOp::SearchResultEntry(lsre) => {
|
|
assert_entry_contains!(
|
|
lsre,
|
|
"spn=testperson1@example.com,dc=example,dc=com",
|
|
(Attribute::Name, "testperson1"),
|
|
(
|
|
Attribute::EntryDn,
|
|
"spn=testperson1@example.com,dc=example,dc=com"
|
|
),
|
|
(Attribute::UidNumber, "12345"),
|
|
(Attribute::LdapKeys, ssh_ed25519)
|
|
);
|
|
}
|
|
_ => panic!("Oh no"),
|
|
};
|
|
}
|
|
|
|
#[idm_test]
|
|
async fn test_ldap_token_privilege_granting(
|
|
idms: &IdmServer,
|
|
_idms_delayed: &IdmServerDelayed,
|
|
) {
|
|
// Setup the ldap server
|
|
let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
|
|
|
|
// Prebuild the search req we'll be using this test.
|
|
let sr = SearchRequest {
|
|
msgid: 1,
|
|
base: "dc=example,dc=com".to_string(),
|
|
scope: LdapSearchScope::Subtree,
|
|
filter: LdapFilter::Equality(Attribute::Name.to_string(), "testperson1".to_string()),
|
|
attrs: vec![
|
|
LDAP_ATTR_NAME,
|
|
LDAP_ATTR_MAIL,
|
|
LDAP_ATTR_MAIL_PRIMARY,
|
|
LDAP_ATTR_MAIL_ALTERNATIVE,
|
|
LDAP_ATTR_EMAIL_PRIMARY,
|
|
LDAP_ATTR_EMAIL_ALTERNATIVE,
|
|
]
|
|
.into_iter()
|
|
.map(|s| s.to_string())
|
|
.collect(),
|
|
};
|
|
|
|
let sa_uuid = uuid::uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
|
|
|
|
// Configure the user account that will have the tokens issued.
|
|
// Should be a SERVICE account.
|
|
let apitoken = {
|
|
// Create a service account,
|
|
|
|
let e1 = entry_init!(
|
|
(Attribute::Class, EntryClass::Object.to_value()),
|
|
(Attribute::Class, EntryClass::ServiceAccount.to_value()),
|
|
(Attribute::Class, EntryClass::Account.to_value()),
|
|
(Attribute::Uuid, Value::Uuid(sa_uuid)),
|
|
(Attribute::Name, Value::new_iname("service_permission_test")),
|
|
(
|
|
Attribute::DisplayName,
|
|
Value::new_utf8s("service_permission_test")
|
|
)
|
|
);
|
|
|
|
// Setup a person with an email
|
|
let e2 = entry_init!(
|
|
(Attribute::Class, EntryClass::Object.to_value()),
|
|
(Attribute::Class, EntryClass::Person.to_value()),
|
|
(Attribute::Class, EntryClass::Account.to_value()),
|
|
(Attribute::Class, EntryClass::PosixAccount.to_value()),
|
|
(Attribute::Name, Value::new_iname("testperson1")),
|
|
(
|
|
Attribute::Mail,
|
|
Value::EmailAddress("testperson1@example.com".to_string(), true)
|
|
),
|
|
(
|
|
Attribute::Mail,
|
|
Value::EmailAddress("testperson1.alternative@example.com".to_string(), false)
|
|
),
|
|
(Attribute::Description, Value::new_utf8s("testperson1")),
|
|
(Attribute::DisplayName, Value::new_utf8s("testperson1")),
|
|
(Attribute::GidNumber, Value::new_uint32(12345)),
|
|
(Attribute::LoginShell, Value::new_iutf8("/bin/zsh"))
|
|
);
|
|
|
|
// Setup an access control for the service account to view mail attrs.
|
|
|
|
let ct = duration_from_epoch_now();
|
|
|
|
let mut server_txn = idms.proxy_write(ct).await.unwrap();
|
|
let ce = CreateEvent::new_internal(vec![e1, e2]);
|
|
assert!(server_txn.qs_write.create(&ce).is_ok());
|
|
|
|
// idm_people_read_priv
|
|
let me = ModifyEvent::new_internal_invalid(
|
|
filter!(f_eq(
|
|
Attribute::Name,
|
|
PartialValue::new_iname("idm_people_pii_read")
|
|
)),
|
|
ModifyList::new_list(vec![Modify::Present(
|
|
Attribute::Member,
|
|
Value::Refer(sa_uuid),
|
|
)]),
|
|
);
|
|
assert!(server_txn.qs_write.modify(&me).is_ok());
|
|
|
|
// Issue a token
|
|
// make it purpose = ldap <- currently purpose isn't supported,
|
|
// it's an idea for future.
|
|
let gte = GenerateApiTokenEvent::new_internal(sa_uuid, "TestToken", None);
|
|
|
|
let apitoken = server_txn
|
|
.service_account_generate_api_token(>e, ct)
|
|
.expect("Failed to create new apitoken");
|
|
|
|
assert!(server_txn.commit().is_ok());
|
|
|
|
apitoken
|
|
};
|
|
|
|
// assert the token fails on non-ldap events token-xchg <- currently
|
|
// we don't have purpose so this isn't tested.
|
|
|
|
// Bind with anonymous, search and show mail attr isn't accessible.
|
|
let anon_lbt = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
|
|
assert_eq!(
|
|
anon_lbt.effective_session,
|
|
LdapSession::UnixBind(UUID_ANONYMOUS)
|
|
);
|
|
|
|
let r1 = ldaps
|
|
.do_search(idms, &sr, &anon_lbt, Source::Internal)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(r1.len(), 2);
|
|
match &r1[0].op {
|
|
LdapOp::SearchResultEntry(lsre) => {
|
|
assert_entry_contains!(
|
|
lsre,
|
|
"spn=testperson1@example.com,dc=example,dc=com",
|
|
(Attribute::Name, "testperson1")
|
|
);
|
|
}
|
|
_ => panic!("Oh no"),
|
|
};
|
|
|
|
// Inspect the token to get its uuid out.
|
|
let jws_verifier = JwsDangerReleaseWithoutVerify::default();
|
|
|
|
let apitoken_inner = jws_verifier
|
|
.verify(&apitoken)
|
|
.unwrap()
|
|
.from_json::<ApiToken>()
|
|
.unwrap();
|
|
|
|
// Bind using the token as a DN
|
|
let sa_lbt = ldaps
|
|
.do_bind(idms, "dn=token", &apitoken.to_string())
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(
|
|
sa_lbt.effective_session,
|
|
LdapSession::ApiToken(apitoken_inner.clone())
|
|
);
|
|
|
|
// Bind using the token as a pw
|
|
let sa_lbt = ldaps
|
|
.do_bind(idms, "", &apitoken.to_string())
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(
|
|
sa_lbt.effective_session,
|
|
LdapSession::ApiToken(apitoken_inner)
|
|
);
|
|
|
|
// Search and retrieve mail that's now accessible.
|
|
let r1 = ldaps
|
|
.do_search(idms, &sr, &sa_lbt, Source::Internal)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(r1.len(), 2);
|
|
match &r1[0].op {
|
|
LdapOp::SearchResultEntry(lsre) => {
|
|
assert_entry_contains!(
|
|
lsre,
|
|
"spn=testperson1@example.com,dc=example,dc=com",
|
|
(Attribute::Name, "testperson1"),
|
|
(Attribute::Mail, "testperson1@example.com"),
|
|
(Attribute::Mail, "testperson1.alternative@example.com"),
|
|
(LDAP_ATTR_MAIL_PRIMARY, "testperson1@example.com"),
|
|
(
|
|
LDAP_ATTR_MAIL_ALTERNATIVE,
|
|
"testperson1.alternative@example.com"
|
|
),
|
|
(LDAP_ATTR_EMAIL_PRIMARY, "testperson1@example.com"),
|
|
(
|
|
LDAP_ATTR_EMAIL_ALTERNATIVE,
|
|
"testperson1.alternative@example.com"
|
|
)
|
|
);
|
|
}
|
|
_ => panic!("Oh no"),
|
|
};
|
|
|
|
// ======= test with a substring search
|
|
|
|
let sr = SearchRequest {
|
|
msgid: 2,
|
|
base: "dc=example,dc=com".to_string(),
|
|
scope: LdapSearchScope::Subtree,
|
|
filter: LdapFilter::And(vec![
|
|
LdapFilter::Equality(Attribute::Class.to_string(), "posixAccount".to_string()),
|
|
LdapFilter::Substring(
|
|
LDAP_ATTR_MAIL.to_string(),
|
|
LdapSubstringFilter {
|
|
initial: None,
|
|
any: vec![],
|
|
final_: Some("@example.com".to_string()),
|
|
},
|
|
),
|
|
]),
|
|
attrs: vec![
|
|
LDAP_ATTR_NAME,
|
|
LDAP_ATTR_MAIL,
|
|
LDAP_ATTR_MAIL_PRIMARY,
|
|
LDAP_ATTR_MAIL_ALTERNATIVE,
|
|
]
|
|
.into_iter()
|
|
.map(|s| s.to_string())
|
|
.collect(),
|
|
};
|
|
|
|
let r1 = ldaps
|
|
.do_search(idms, &sr, &sa_lbt, Source::Internal)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(r1.len(), 2);
|
|
match &r1[0].op {
|
|
LdapOp::SearchResultEntry(lsre) => {
|
|
assert_entry_contains!(
|
|
lsre,
|
|
"spn=testperson1@example.com,dc=example,dc=com",
|
|
(Attribute::Name, "testperson1"),
|
|
(Attribute::Mail, "testperson1@example.com"),
|
|
(Attribute::Mail, "testperson1.alternative@example.com"),
|
|
(LDAP_ATTR_MAIL_PRIMARY, "testperson1@example.com"),
|
|
(
|
|
LDAP_ATTR_MAIL_ALTERNATIVE,
|
|
"testperson1.alternative@example.com"
|
|
)
|
|
);
|
|
}
|
|
_ => panic!("Oh no"),
|
|
};
|
|
}
|
|
|
|
#[idm_test]
|
|
async fn test_ldap_virtual_attribute_with_all_attr_search(
|
|
idms: &IdmServer,
|
|
_idms_delayed: &IdmServerDelayed,
|
|
) {
|
|
let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
|
|
|
|
let acct_uuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
|
|
|
|
// Setup a user we want to check.
|
|
{
|
|
let e1 = entry_init!(
|
|
(Attribute::Class, EntryClass::Person.to_value()),
|
|
(Attribute::Class, EntryClass::Account.to_value()),
|
|
(Attribute::Name, Value::new_iname("testperson1")),
|
|
(Attribute::Uuid, Value::Uuid(acct_uuid)),
|
|
(Attribute::Description, Value::new_utf8s("testperson1")),
|
|
(Attribute::DisplayName, Value::new_utf8s("testperson1"))
|
|
);
|
|
|
|
let mut server_txn = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
|
|
assert!(server_txn
|
|
.qs_write
|
|
.internal_create(vec![e1])
|
|
.and_then(|_| server_txn.commit())
|
|
.is_ok());
|
|
}
|
|
|
|
// Setup the anonymous login.
|
|
let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
|
|
assert_eq!(
|
|
anon_t.effective_session,
|
|
LdapSession::UnixBind(UUID_ANONYMOUS)
|
|
);
|
|
|
|
// Check that when we request a virtual attr by name *and* all_attrs we get all the requested values.
|
|
let sr = SearchRequest {
|
|
msgid: 1,
|
|
base: "dc=example,dc=com".to_string(),
|
|
scope: LdapSearchScope::Subtree,
|
|
filter: LdapFilter::Equality(Attribute::Name.to_string(), "testperson1".to_string()),
|
|
attrs: vec![
|
|
"*".to_string(),
|
|
// Already being returned
|
|
LDAP_ATTR_NAME.to_string(),
|
|
// This is a virtual attribute
|
|
Attribute::EntryUuid.to_string(),
|
|
],
|
|
};
|
|
let r1 = ldaps
|
|
.do_search(idms, &sr, &anon_t, Source::Internal)
|
|
.await
|
|
.unwrap();
|
|
|
|
// The result, and the ldap proto success msg.
|
|
assert_eq!(r1.len(), 2);
|
|
match &r1[0].op {
|
|
LdapOp::SearchResultEntry(lsre) => {
|
|
assert_entry_contains!(
|
|
lsre,
|
|
"spn=testperson1@example.com,dc=example,dc=com",
|
|
(Attribute::Name, "testperson1"),
|
|
(Attribute::DisplayName, "testperson1"),
|
|
(Attribute::Uuid, "cc8e95b4-c24f-4d68-ba54-8bed76f63930"),
|
|
(Attribute::EntryUuid, "cc8e95b4-c24f-4d68-ba54-8bed76f63930")
|
|
);
|
|
}
|
|
_ => panic!("Oh no"),
|
|
};
|
|
}
|
|
|
|
// Test behaviour of the 1.1 attribute.
|
|
#[idm_test]
|
|
async fn test_ldap_one_dot_one_attribute(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
|
|
let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
|
|
|
|
let acct_uuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
|
|
|
|
// Setup a user we want to check.
|
|
{
|
|
let e1 = entry_init!(
|
|
(Attribute::Class, EntryClass::Person.to_value()),
|
|
(Attribute::Class, EntryClass::Account.to_value()),
|
|
(Attribute::Name, Value::new_iname("testperson1")),
|
|
(Attribute::Uuid, Value::Uuid(acct_uuid)),
|
|
(Attribute::Description, Value::new_utf8s("testperson1")),
|
|
(Attribute::DisplayName, Value::new_utf8s("testperson1"))
|
|
);
|
|
|
|
let mut server_txn = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
|
|
assert!(server_txn
|
|
.qs_write
|
|
.internal_create(vec![e1])
|
|
.and_then(|_| server_txn.commit())
|
|
.is_ok());
|
|
}
|
|
|
|
// Setup the anonymous login.
|
|
let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
|
|
assert_eq!(
|
|
anon_t.effective_session,
|
|
LdapSession::UnixBind(UUID_ANONYMOUS)
|
|
);
|
|
|
|
// If we request only 1.1, we get no attributes.
|
|
let sr = SearchRequest {
|
|
msgid: 1,
|
|
base: "dc=example,dc=com".to_string(),
|
|
scope: LdapSearchScope::Subtree,
|
|
filter: LdapFilter::Equality(Attribute::Name.to_string(), "testperson1".to_string()),
|
|
attrs: vec!["1.1".to_string()],
|
|
};
|
|
let r1 = ldaps
|
|
.do_search(idms, &sr, &anon_t, Source::Internal)
|
|
.await
|
|
.unwrap();
|
|
|
|
// The result, and the ldap proto success msg.
|
|
assert_eq!(r1.len(), 2);
|
|
match &r1[0].op {
|
|
LdapOp::SearchResultEntry(lsre) => {
|
|
assert_eq!(
|
|
lsre.dn.as_str(),
|
|
"spn=testperson1@example.com,dc=example,dc=com"
|
|
);
|
|
assert!(lsre.attributes.is_empty());
|
|
}
|
|
_ => panic!("Oh no"),
|
|
};
|
|
|
|
// If we request 1.1 and another attr, 1.1 is IGNORED.
|
|
let sr = SearchRequest {
|
|
msgid: 1,
|
|
base: "dc=example,dc=com".to_string(),
|
|
scope: LdapSearchScope::Subtree,
|
|
filter: LdapFilter::Equality(Attribute::Name.to_string(), "testperson1".to_string()),
|
|
attrs: vec![
|
|
"1.1".to_string(),
|
|
// This should be present.
|
|
Attribute::EntryUuid.to_string(),
|
|
],
|
|
};
|
|
let r1 = ldaps
|
|
.do_search(idms, &sr, &anon_t, Source::Internal)
|
|
.await
|
|
.unwrap();
|
|
|
|
// The result, and the ldap proto success msg.
|
|
assert_eq!(r1.len(), 2);
|
|
match &r1[0].op {
|
|
LdapOp::SearchResultEntry(lsre) => {
|
|
assert_entry_contains!(
|
|
lsre,
|
|
"spn=testperson1@example.com,dc=example,dc=com",
|
|
(Attribute::EntryUuid, "cc8e95b4-c24f-4d68-ba54-8bed76f63930")
|
|
);
|
|
}
|
|
_ => panic!("Oh no"),
|
|
};
|
|
}
|
|
|
|
#[idm_test]
|
|
async fn test_ldap_rootdse_basedn_change(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
|
|
let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
|
|
|
|
let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
|
|
assert_eq!(
|
|
anon_t.effective_session,
|
|
LdapSession::UnixBind(UUID_ANONYMOUS)
|
|
);
|
|
|
|
let sr = SearchRequest {
|
|
msgid: 1,
|
|
base: "".to_string(),
|
|
scope: LdapSearchScope::Base,
|
|
filter: LdapFilter::Present(Attribute::ObjectClass.to_string()),
|
|
attrs: vec!["*".to_string()],
|
|
};
|
|
let r1 = ldaps
|
|
.do_search(idms, &sr, &anon_t, Source::Internal)
|
|
.await
|
|
.unwrap();
|
|
|
|
trace!(?r1);
|
|
|
|
// The result, and the ldap proto success msg.
|
|
assert_eq!(r1.len(), 2);
|
|
match &r1[0].op {
|
|
LdapOp::SearchResultEntry(lsre) => {
|
|
assert_entry_contains!(
|
|
lsre,
|
|
"",
|
|
(Attribute::ObjectClass, "top"),
|
|
("vendorname", "Kanidm Project"),
|
|
("supportedldapversion", "3"),
|
|
("defaultnamingcontext", "dc=example,dc=com")
|
|
);
|
|
}
|
|
_ => panic!("Oh no"),
|
|
};
|
|
|
|
drop(ldaps);
|
|
|
|
// Change the domain basedn
|
|
|
|
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
|
|
// make the admin a valid posix account
|
|
let me_posix = ModifyEvent::new_internal_invalid(
|
|
filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_DOMAIN_INFO))),
|
|
ModifyList::new_purge_and_set(
|
|
Attribute::DomainLdapBasedn,
|
|
Value::new_iutf8("o=kanidmproject"),
|
|
),
|
|
);
|
|
assert!(idms_prox_write.qs_write.modify(&me_posix).is_ok());
|
|
|
|
assert!(idms_prox_write.commit().is_ok());
|
|
|
|
// Now re-test
|
|
let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
|
|
|
|
let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
|
|
assert_eq!(
|
|
anon_t.effective_session,
|
|
LdapSession::UnixBind(UUID_ANONYMOUS)
|
|
);
|
|
|
|
let sr = SearchRequest {
|
|
msgid: 1,
|
|
base: "".to_string(),
|
|
scope: LdapSearchScope::Base,
|
|
filter: LdapFilter::Present(Attribute::ObjectClass.to_string()),
|
|
attrs: vec!["*".to_string()],
|
|
};
|
|
let r1 = ldaps
|
|
.do_search(idms, &sr, &anon_t, Source::Internal)
|
|
.await
|
|
.unwrap();
|
|
|
|
trace!(?r1);
|
|
|
|
// The result, and the ldap proto success msg.
|
|
assert_eq!(r1.len(), 2);
|
|
match &r1[0].op {
|
|
LdapOp::SearchResultEntry(lsre) => {
|
|
assert_entry_contains!(
|
|
lsre,
|
|
"",
|
|
(Attribute::ObjectClass, "top"),
|
|
("vendorname", "Kanidm Project"),
|
|
("supportedldapversion", "3"),
|
|
("defaultnamingcontext", "o=kanidmproject")
|
|
);
|
|
}
|
|
_ => panic!("Oh no"),
|
|
};
|
|
}
|
|
|
|
#[idm_test]
|
|
async fn test_ldap_sssd_compat(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
|
|
let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
|
|
|
|
let acct_uuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
|
|
|
|
// Setup a user we want to check.
|
|
{
|
|
let e1 = entry_init!(
|
|
(Attribute::Class, EntryClass::Person.to_value()),
|
|
(Attribute::Class, EntryClass::Account.to_value()),
|
|
(Attribute::Class, EntryClass::PosixAccount.to_value()),
|
|
(Attribute::Name, Value::new_iname("testperson1")),
|
|
(Attribute::Uuid, Value::Uuid(acct_uuid)),
|
|
(Attribute::GidNumber, Value::Uint32(12345)),
|
|
(Attribute::Description, Value::new_utf8s("testperson1")),
|
|
(Attribute::DisplayName, Value::new_utf8s("testperson1"))
|
|
);
|
|
|
|
let mut server_txn = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
|
|
assert!(server_txn
|
|
.qs_write
|
|
.internal_create(vec![e1])
|
|
.and_then(|_| server_txn.commit())
|
|
.is_ok());
|
|
}
|
|
|
|
// Setup the anonymous login.
|
|
let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
|
|
assert_eq!(
|
|
anon_t.effective_session,
|
|
LdapSession::UnixBind(UUID_ANONYMOUS)
|
|
);
|
|
|
|
// SSSD tries to just search for silly attrs all the time. We ignore them.
|
|
let sr = SearchRequest {
|
|
msgid: 1,
|
|
base: "dc=example,dc=com".to_string(),
|
|
scope: LdapSearchScope::Subtree,
|
|
filter: LdapFilter::And(vec![
|
|
LdapFilter::Equality(Attribute::Class.to_string(), "sudohost".to_string()),
|
|
LdapFilter::Substring(
|
|
Attribute::SudoHost.to_string(),
|
|
LdapSubstringFilter {
|
|
initial: Some("a".to_string()),
|
|
any: vec!["x".to_string()],
|
|
final_: Some("z".to_string()),
|
|
},
|
|
),
|
|
]),
|
|
attrs: vec![
|
|
"*".to_string(),
|
|
// Already being returned
|
|
LDAP_ATTR_NAME.to_string(),
|
|
// This is a virtual attribute
|
|
Attribute::EntryUuid.to_string(),
|
|
],
|
|
};
|
|
let r1 = ldaps
|
|
.do_search(idms, &sr, &anon_t, Source::Internal)
|
|
.await
|
|
.unwrap();
|
|
|
|
// Empty results and ldap proto success msg.
|
|
assert_eq!(r1.len(), 1);
|
|
|
|
// Second search
|
|
|
|
let sr = SearchRequest {
|
|
msgid: 1,
|
|
base: "dc=example,dc=com".to_string(),
|
|
scope: LdapSearchScope::Subtree,
|
|
filter: LdapFilter::Equality(Attribute::Name.to_string(), "testperson1".to_string()),
|
|
attrs: vec![
|
|
"uid".to_string(),
|
|
"uidNumber".to_string(),
|
|
"gidNumber".to_string(),
|
|
"gecos".to_string(),
|
|
"cn".to_string(),
|
|
"entryuuid".to_string(),
|
|
],
|
|
};
|
|
let r1 = ldaps
|
|
.do_search(idms, &sr, &anon_t, Source::Internal)
|
|
.await
|
|
.unwrap();
|
|
|
|
trace!(?r1);
|
|
|
|
// The result, and the ldap proto success msg.
|
|
assert_eq!(r1.len(), 2);
|
|
match &r1[0].op {
|
|
LdapOp::SearchResultEntry(lsre) => {
|
|
assert_entry_contains!(
|
|
lsre,
|
|
"spn=testperson1@example.com,dc=example,dc=com",
|
|
(Attribute::Uid, "testperson1"),
|
|
(Attribute::Cn, "testperson1"),
|
|
(Attribute::Gecos, "testperson1"),
|
|
(Attribute::UidNumber, "12345"),
|
|
(Attribute::GidNumber, "12345"),
|
|
(Attribute::EntryUuid, "cc8e95b4-c24f-4d68-ba54-8bed76f63930")
|
|
);
|
|
}
|
|
_ => panic!("Oh no"),
|
|
};
|
|
}
|
|
|
|
#[idm_test]
|
|
async fn test_ldap_compare_request(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
|
|
let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
|
|
|
|
// Setup a user we want to check.
|
|
{
|
|
let acct_uuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
|
|
|
|
let e1 = entry_init!(
|
|
(Attribute::Class, EntryClass::Person.to_value()),
|
|
(Attribute::Class, EntryClass::Account.to_value()),
|
|
(Attribute::Class, EntryClass::PosixAccount.to_value()),
|
|
(Attribute::Name, Value::new_iname("testperson1")),
|
|
(Attribute::Uuid, Value::Uuid(acct_uuid)),
|
|
(Attribute::GidNumber, Value::Uint32(12345)),
|
|
(Attribute::Description, Value::new_utf8s("testperson1")),
|
|
(Attribute::DisplayName, Value::new_utf8s("testperson1"))
|
|
);
|
|
|
|
let mut server_txn = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
|
|
assert!(server_txn
|
|
.qs_write
|
|
.internal_create(vec![e1])
|
|
.and_then(|_| server_txn.commit())
|
|
.is_ok());
|
|
}
|
|
|
|
// Setup the anonymous login.
|
|
let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
|
|
assert_eq!(
|
|
anon_t.effective_session,
|
|
LdapSession::UnixBind(UUID_ANONYMOUS)
|
|
);
|
|
|
|
#[track_caller]
|
|
fn assert_compare_result(r: &[LdapMsg], code: &LdapResultCode) {
|
|
assert_eq!(r.len(), 1);
|
|
match &r[0].op {
|
|
LdapOp::CompareResult(lcr) => {
|
|
assert_eq!(&lcr.code, code);
|
|
}
|
|
_ => panic!("Oh no"),
|
|
};
|
|
}
|
|
|
|
let cr = CompareRequest {
|
|
msgid: 1,
|
|
entry: "name=testperson1,dc=example,dc=com".to_string(),
|
|
atype: Attribute::Name.to_string(),
|
|
val: "testperson1".to_string(),
|
|
};
|
|
|
|
assert_compare_result(
|
|
&ldaps
|
|
.do_compare(idms, &cr, &anon_t, Source::Internal)
|
|
.await
|
|
.unwrap(),
|
|
&LdapResultCode::CompareTrue,
|
|
);
|
|
|
|
let cr = CompareRequest {
|
|
msgid: 1,
|
|
entry: "name=testperson1,dc=example,dc=com".to_string(),
|
|
atype: Attribute::GidNumber.to_string(),
|
|
val: "12345".to_string(),
|
|
};
|
|
|
|
assert_compare_result(
|
|
&ldaps
|
|
.do_compare(idms, &cr, &anon_t, Source::Internal)
|
|
.await
|
|
.unwrap(),
|
|
&LdapResultCode::CompareTrue,
|
|
);
|
|
|
|
let cr = CompareRequest {
|
|
msgid: 1,
|
|
entry: "name=testperson1,dc=example,dc=com".to_string(),
|
|
atype: Attribute::Name.to_string(),
|
|
val: "other".to_string(),
|
|
};
|
|
assert_compare_result(
|
|
&ldaps
|
|
.do_compare(idms, &cr, &anon_t, Source::Internal)
|
|
.await
|
|
.unwrap(),
|
|
&LdapResultCode::CompareFalse,
|
|
);
|
|
|
|
let cr = CompareRequest {
|
|
msgid: 1,
|
|
entry: "name=other,dc=example,dc=com".to_string(),
|
|
atype: Attribute::Name.to_string(),
|
|
val: "other".to_string(),
|
|
};
|
|
assert_compare_result(
|
|
&ldaps
|
|
.do_compare(idms, &cr, &anon_t, Source::Internal)
|
|
.await
|
|
.unwrap(),
|
|
&LdapResultCode::NoSuchObject,
|
|
);
|
|
|
|
let cr = CompareRequest {
|
|
msgid: 1,
|
|
entry: "invalidentry".to_string(),
|
|
atype: Attribute::Name.to_string(),
|
|
val: "other".to_string(),
|
|
};
|
|
assert!(&ldaps
|
|
.do_compare(idms, &cr, &anon_t, Source::Internal)
|
|
.await
|
|
.is_err());
|
|
|
|
let cr = CompareRequest {
|
|
msgid: 1,
|
|
entry: "name=other,dc=example,dc=com".to_string(),
|
|
atype: "invalid".to_string(),
|
|
val: "other".to_string(),
|
|
};
|
|
assert_eq!(
|
|
&ldaps
|
|
.do_compare(idms, &cr, &anon_t, Source::Internal)
|
|
.await
|
|
.unwrap_err(),
|
|
&OperationError::InvalidAttributeName("invalid".to_string()),
|
|
);
|
|
}
|
|
|
|
#[idm_test]
|
|
async fn test_ldap_maximum_queryable_attributes(
|
|
idms: &IdmServer,
|
|
_idms_delayed: &IdmServerDelayed,
|
|
) {
|
|
// Set the max queryable attrs to 2
|
|
|
|
let mut server_txn = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
|
|
|
|
let set_ldap_maximum_queryable_attrs = ModifyEvent::new_internal_invalid(
|
|
filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_DOMAIN_INFO))),
|
|
ModifyList::new_purge_and_set(Attribute::LdapMaxQueryableAttrs, Value::Uint32(2)),
|
|
);
|
|
assert!(server_txn
|
|
.qs_write
|
|
.modify(&set_ldap_maximum_queryable_attrs)
|
|
.and_then(|_| server_txn.commit())
|
|
.is_ok());
|
|
|
|
let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
|
|
|
|
let usr_uuid = Uuid::new_v4();
|
|
let grp_uuid = Uuid::new_v4();
|
|
let app_uuid = Uuid::new_v4();
|
|
let app_name = "testapp1";
|
|
|
|
// Setup person, group and application
|
|
{
|
|
let e1 = entry_init!(
|
|
(Attribute::Class, EntryClass::Object.to_value()),
|
|
(Attribute::Class, EntryClass::Account.to_value()),
|
|
(Attribute::Class, EntryClass::Person.to_value()),
|
|
(Attribute::Name, Value::new_iname("testperson1")),
|
|
(Attribute::Uuid, Value::Uuid(usr_uuid)),
|
|
(Attribute::Description, Value::new_utf8s("testperson1")),
|
|
(Attribute::DisplayName, Value::new_utf8s("testperson1"))
|
|
);
|
|
|
|
let e2 = entry_init!(
|
|
(Attribute::Class, EntryClass::Object.to_value()),
|
|
(Attribute::Class, EntryClass::Group.to_value()),
|
|
(Attribute::Name, Value::new_iname("testgroup1")),
|
|
(Attribute::Uuid, Value::Uuid(grp_uuid))
|
|
);
|
|
|
|
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))
|
|
);
|
|
|
|
let ct = duration_from_epoch_now();
|
|
let mut server_txn = idms.proxy_write(ct).await.unwrap();
|
|
assert!(server_txn
|
|
.qs_write
|
|
.internal_create(vec![e1, e2, e3])
|
|
.and_then(|_| server_txn.commit())
|
|
.is_ok());
|
|
}
|
|
|
|
// Setup the anonymous login
|
|
let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
|
|
assert_eq!(
|
|
anon_t.effective_session,
|
|
LdapSession::UnixBind(UUID_ANONYMOUS)
|
|
);
|
|
|
|
let invalid_search = SearchRequest {
|
|
msgid: 1,
|
|
base: "dc=example,dc=com".to_string(),
|
|
scope: LdapSearchScope::Subtree,
|
|
filter: LdapFilter::Present(Attribute::ObjectClass.to_string()),
|
|
attrs: vec![
|
|
"objectClass".to_string(),
|
|
"cn".to_string(),
|
|
"givenName".to_string(),
|
|
],
|
|
};
|
|
|
|
let valid_search = SearchRequest {
|
|
msgid: 1,
|
|
base: "dc=example,dc=com".to_string(),
|
|
scope: LdapSearchScope::Subtree,
|
|
filter: LdapFilter::Present(Attribute::ObjectClass.to_string()),
|
|
attrs: vec!["objectClass: person".to_string()],
|
|
};
|
|
|
|
let invalid_res: Result<Vec<LdapMsg>, OperationError> = ldaps
|
|
.do_search(idms, &invalid_search, &anon_t, Source::Internal)
|
|
.await;
|
|
|
|
let valid_res: Result<Vec<LdapMsg>, OperationError> = ldaps
|
|
.do_search(idms, &valid_search, &anon_t, Source::Internal)
|
|
.await;
|
|
|
|
assert_eq!(invalid_res, Err(OperationError::ResourceLimit));
|
|
assert!(valid_res.is_ok());
|
|
}
|
|
}
|